Business Central Build Pipeline on Self Hosted Agent

Published on 28 August 2023 at 07:00

Introduction

In the previous post, a demonstration was provided on how to construct an application for Microsoft Dynamics 365 Business Central using the Microsoft Hosted Agent.

In this installment, the focus shifts towards illustrating the process of building an application using a Self Hosted Agent. While the core script remains largely consistent, there are a few nuanced differences that deserve attention.

The previous post is a cool thing for simple PTE applications, but when dealing with OnPrem environments, things usually get complicated with dependency apps. At NavBiz, we have a considerable number of modules that depend on certain core modules, such as localization for the Croatian market. Many of these applications are located on AppSource. For building applications, we are using Self Hosted Agents (Azure Virtual Machines) the same machines we are using for DEV environement where consultants can test the features(for real development we are using docker container ofc.), and for deployment, we use Self Hosted Agents installed on client machines where BC services are running. For AppSource apps, during the app building process, we run AlValidation, and if that passes successfully, we proceed with the Automatic AppSource Submission of Business Central apps. For Dev environment we have develop branch, for UAT environment we have UAT branch, main is for PROD environment and appsource branch is for AppSource.

The description of all of this will follow in the next several blog posts.

Prerequisites

  • Azure DevOps account
    • Windows Self Hosted Agent on Azure Dev Ops

Detailed inctructions can be found here

 

After that you have to install latest version of Docker Dekstop on your agent.

One repo for all apps config

We have a single repository where all the scripts for build & release and all the configuration files for all apps

You can notice that for each app, we also have the repository name specified, which will be necessary later for downloading symbols.

Creating container with latest artifact every morning

Why?

Because the application build, when creating a container, takes 10-15 minutes, whereas when the application build is performed within an already created container, the process takes 2-3 minutes.

That's why :)

Source code: 

function CheckIfContainerIsRunning() {
    param(
        [string] $ContainerName,
        [bool] $Running
    )
    $attempt = 0
    while (!$Running) {
        Try {
            Get-BcContainerServerConfiguration -ContainerName $ContainerName | Out-Null
            $State = Get-BcContainerTenants -containerName $ContainerName
            write-host "##[section]Current state" $State.state
            if (($State.state -eq 'Operational') -or ($State.state -eq 'Mounted')) { 
                $Running = $true
            }
            else {
                Start-sleep 1
                $attempt += 1
                write-host "##[section]Service in" $ContainerName "not running or tenant is not in Operational state, attempt" $attempt
            }
            if ($attempt -gt 120) {
                write-host "##[section]Attempt of startnig container is greater than" $attempt "removing container"
                Remove-BcContainer -containerName $ContainerName
                break
            }
        }
        Catch {
            Start-sleep 1
            $attempt += 1
            write-host "##[section]Service in" $ContainerName "not running or tenant is not in Operational state, attempt" $attempt

            if ($attempt -gt 120) {
                write-host "##[section]Attempt of startnig container is greater than" $attempt "removing container"
                Remove-BcContainer -containerName $ContainerName
                break
            }
        }
    }
    return $Running
}



#install bccontainerhelper
$module = Get-InstalledModule -Name bccontainerhelper -ErrorAction Ignore
if ($module) {
    $versionStr = $module.Version.ToString()
    Write-Host "##[section]BcContainerHelper $VersionStr is installed"
    Write-Host "##[section]Determine latest BcContainerHelper version"
    $latestVersion = (Find-Module -Name bccontainerhelper).Version
    $latestVersionStr = $latestVersion.ToString()
    Write-Host "##[section]BcContainerHelper $latestVersionStr is the latest version"
    if ($latestVersion -gt $module.Version) {
        Write-Host "##[command]Updating BcContainerHelper to $latestVersionStr"
        Update-Module -Name bccontainerhelper -Force -RequiredVersion $latestVersionStr
        Write-Host "##[section]BcContainerHelper updated"
    }
}
else {
    Write-Host "##[command]Installing BcContainerHelper"
    Install-Module -Name bccontainerhelper -Force
    $module = Get-InstalledModule -Name bccontainerhelper -ErrorAction Ignore
    $versionStr = $module.Version.ToString()
    Write-Host "##[section]BcContainerHelper $VersionStr installed"
}
#install bccontainerhelper

#creating container
$RepositoryDirectory = Get-Location

Write-Host "##[command]Starting Docker Desktop and switching to windows containers"
Set-Location 'C:\Program Files\Docker\Docker'
& '.\Docker Desktop.exe'

Set-Location "C:\Program Files\Docker\Docker"
& '.\DockerCli.exe' -SwitchWindowsEngine

Get-Process *docker*
Get-Service *docker*

Set-Location $RepositoryDirectory

Start-sleep 30
$containers = Get-BcContainers
if ($containers) {
    foreach ($container in $containers) {
        docker container stop $container
        docker rm $container
    }
}

docker system prune --all --force

$ContainerName = $env:agent_name
$password = ConvertTo-SecureString $env:PASSWORD -AsPlainText -Force
$Credential = New-Object System.Management.Automation.PSCredential ($env:USERNAME, $password)
$artifactUrl = Get-BCArtifactUrl -country w1 -select Latest -storageAccount bcartifacts -type Sandbox
$AdditionalParameters = @()
$AdditionalParameters += '--volume "{0}:{1}"' -f 'C:\agent\_work\', "C:\agent\_work"
$AdditionalParameters += '--volume "{0}:{1}"' -f 'C:\agent1\_work\', "C:\agent1\_work"

$Params = @{}
$Params += @{ accept_eula = $true }
$Params += @{ artifactUrl = $artifactUrl }
$Params += @{ containerName = $ContainerName }
$Params += @{ auth = 'NavUserPassword' }
$Params += @{ credential = $Credential }
$Params += @{ isolation = 'process' }
$Params += @{ accept_outdated = $true }
$Params += @{ useBestContainerOS = $true }
$Params += @{ additionalParameters = $AdditionalParameters }

New-BcContainer @Params -shortcuts None

$running = $false
$running = CheckIfContainerIsRunning -ContainerName $ContainerName -Running $running
if (!$running) {
    Write-Error -Message "Could not start container." -ErrorAction Stop
    throw "Could not start container."
}
#creating container

Script Path: 

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

CICD.yml

In your app create yml file

Notice that we have linked Variable group

Some of variables are unused (cleanup needed)

Source code:

variables:
  - group: BuildAndRelease_v21
  
resources:
  repositories:
  - repository: Build and Release
    type: git
    name: Business Central/Build and Release
    ref: master

trigger:
  branches:
    include:
      - develop
      - uat
      - main
      - appsource

pool:
  name: NavBizDev01

workspace:
  clean: all

steps:
- checkout: self
- checkout: Build and Release

- task: DockerInstaller@0
  displayName: Docker Installer
  inputs:
    dockerVersion: 17.09.0-ce
    releaseType: stable


- task: PowerShell@2
  displayName: 'Build AL application'
  inputs:
    targetType: filePath
    filePath: 'Build and Release\CompileApp_v22.ps1'
    workingDirectory: '$(Build.Repository.Name)'

- task: PowerShell@2
  condition: eq(variables['Build.SourceBranch'], 'refs/heads/appsource')
  displayName: 'AL Validation'
  inputs:
    targetType: filePath
    filePath: 'Build and Release\ALValidation.ps1'
    workingDirectory: '$(Build.Repository.Name)'
            
- task: PublishBuildArtifacts@1
  inputs:
    pathtoPublish: '$(Build.StagingDirectory)'
    artifactName: Artifacts

CompileApp.ps1

Create power shell script for compiling app in app repo

Source code:

function CheckIfContainerIsRunning() {
    param(
        [string] $ContainerName,
        [bool] $Running
    )
    $attempt = 0
    while (!$Running) {
        Try {
            Get-BcContainerServerConfiguration -ContainerName $ContainerName | Out-Null
            $State = Get-BcContainerTenants -containerName $ContainerName
            write-host "##[section]Current state" $State.state
            if (($State.state -eq 'Operational') -or ($State.state -eq 'Mounted')) { 
                $Running = $true
            }
            else {
                Start-sleep 1
                $attempt += 1
                write-host "##[section]Service in" $ContainerName "not running or tenant is not in Operational state, attempt" $attempt
            }
            if ($attempt -gt 120) {
                write-host "##[section]Attempt of startnig container is greater than" $attempt "removing container"
                Remove-BcContainer -containerName $ContainerName
                break
            }
        }
        Catch {
            Start-sleep 1
            $attempt += 1
            write-host "##[section]Service in" $ContainerName "not running or tenant is not in Operational state, attempt" $attempt

            if ($attempt -gt 120) {
                write-host "##[section]Attempt of startnig container is greater than" $attempt "removing container"
                Remove-BcContainer -containerName $ContainerName
                break
            }
        }
    }
    return $Running
}

#creating container
$RepositoryDirectory = Get-Location

Get-Process *docker*
Get-Service *docker*

Write-Host "##[command]Starting Docker Desktop and switching to windows containers"
Set-Location 'C:\Program Files\Docker\Docker'
& '.\Docker Desktop.exe'

Set-Location "C:\Program Files\Docker\Docker"
& '.\DockerCli.exe' -SwitchWindowsEngine

Set-Location $RepositoryDirectory

$ContainerName = $env:agent_name
$password = ConvertTo-SecureString $env:PASSWORD -AsPlainText -Force
$Credential = New-Object System.Management.Automation.PSCredential ($env:USERNAME, $password)

$artifactUrl = Get-BCArtifactUrl -country w1 -select Latest -storageAccount bcartifacts -type Sandbox
$createContainer = $false
$containers = Get-BcContainers
if ($containers -and $containers.Contains($ContainerName)) {
    $containerartifacturl = Get-BcContainerArtifactUrl -containerName $ContainerName
    write-host "##[section]Container" $ContainerName "exist with artifacts" $containerartifacturl
    if ($artifactUrl -ne $containerartifacturl) {
        $createContainer = $true 
        Write-host "##[section]There is newer artifacts, need to create new container with artifacts" $artifactUrl 
    }
}
else {
    write-host "##[section]Container" $ContainerName "does not exist, need to create container"
    $createContainer = $true
}

$running = $false
if (!$createContainer) {
    docker stop $ContainerName
    docker start $ContainerName
    $running = CheckIfContainerIsRunning -ContainerName $ContainerName -Running $running
}

if (!$running) {
    $createContainer = $true
}

if ($createContainer) {
    $AdditionalParameters = @()
    $AdditionalParameters += '--volume "{0}:{1}"' -f 'C:\agent\_work\', "C:\agent\_work"
    $AdditionalParameters += '--volume "{0}:{1}"' -f 'C:\agent1\_work\', "C:\agent1\_work"

    $Params = @{}
    $Params += @{ accept_eula = $true }
    $Params += @{ artifactUrl = $artifactUrl }
    $Params += @{ containerName = $ContainerName }
    $Params += @{ auth = 'NavUserPassword' }
    $Params += @{ credential = $Credential }
    $Params += @{ isolation = 'process' }
    $Params += @{ accept_outdated = $true }
    $Params += @{ useBestContainerOS = $true }
    $Params += @{ additionalParameters = $AdditionalParameters }

    New-BcContainer @Params -shortcuts None
}

$running = CheckIfContainerIsRunning -ContainerName $ContainerName -Running $running
if (!$running) {
    Write-Error -Message "Could not start container." -ErrorAction Stop
    throw "Could not start container."
}
#creating container

#increase app version
$app = (Get-Content "app.json" -Encoding UTF8 | ConvertFrom-Json)
$existingVersion = $app.version -as [version]
$versionBuild = Get-Date -Format "yyyyMMdd"
$versionRevision = Get-Date -Format "HHmmss"
$nextVersion = [version]::new($existingVersion.Major, $existingVersion.Minor, $versionBuild, $versionRevision)
$app.version = "$nextVersion"
$app | ConvertTo-Json | Set-Content app.json
write-host "##[section]Version increased to "$nextVersion
#increase app version

#downloading dependencies
#write-host "##[section]Downloading dependencies"

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

$destinationPath = Join-Path $RepositoryDirectory -ChildPath(".alpackages\")
New-Item -Path $destinationPath -ItemType "directory" -Force
foreach ($Dependency in $App.dependencies) {
    if ($Dependency.publisher -ne "Microsoft") {
        #getting repository name from config
        $settings = (Get-Content -Path (Join-Path $env:Build_SourcesDirectory -ChildPath("Build and Release\Config\v22\" + $env:ConfigFile)) -Encoding UTF8 | ConvertFrom-Json)
        $settings = $settings.Apps | Where-Object { $_.id -eq $Dependency.id }
        $repositoryName = $settings.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 dependency 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 = $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
        Expand-Archive -LiteralPath $OutFile -DestinationPath $repositoryName
        $sourcepath = Join-Path $RepositoryDirectory -ChildPath($repositoryName + "\" + $ArtifactName + "\*")
        write-host "##[section]Moving app" $repositoryName "to .alpackages folder"
        Copy-Item -Path $sourcepath -Include *app -Destination $destinationPath -Recurse
        #downloading dependency app
    }
}

$Path = Join-Path $RepositoryDirectory  -ChildPath(".alpackages\")
$AllAppFiles = Get-ChildItem -Path $Path -Filter "*.app"

$installApps = ""
foreach ($File in $AllAppFiles) {
    $installApps += $File.FullName + ","
}

if ($installApps) {
    $installApps = $installApps.Remove($installApps.Length - 1, 1)
}
#downloading dependencies

#compile app
$netpackages = Join-Path -Path $RepositoryDirectory -ChildPath(".netpackages")
$ruleset = (Join-Path $RepositoryDirectory -ChildPath("company.ruleset.json"))

write-host "##[command]Compiling app" $app.Name $app.Version
if (Test-Path -Path $netpackages) { 
    Compile-AppInBcContainer -appProjectFolder $RepositoryDirectory -assemblyProbingPaths $netpackages -containerName $ContainerName -credential $Credential -EnableAppSourceCop -GenerateReportLayout Yes -ReportSuppressedDiagnostics -rulesetFile $ruleset
    #Run-AlPipeline -appFolders $RepositoryDirectory -baseFolder $RepositoryDirectory -containerName $ContainerName -credential $Credential -rulesetFile $ruleset -doNotPublishApps -installApps $installApps -assemblyProbingPaths $netpackages -keepContainer -reUseContainer $reUseContainer
}
else { 
    Compile-AppInBcContainer -appProjectFolder $RepositoryDirectory -containerName $ContainerName -credential $Credential -EnableAppSourceCop -GenerateReportLayout Yes -ReportSuppressedDiagnostics -rulesetFile $ruleset
    #Run-AlPipeline -appFolders $RepositoryDirectory -baseFolder $RepositoryDirectory -containerName $ContainerName -credential $Credential -rulesetFile $ruleset -doNotPublishApps -installApps $installApps -keepContainer -reUseContainer $reUseContainer
}
#compile app

#copy app to build staging directory
write-host "##[section]Moving app to build staging directory"
Copy-Item -Path (Join-Path $RepositoryDirectory -ChildPath("\output\" + $app.publisher + "_" + $app.Name + "_" + $app.Version + ".app")) -Destination $env:Build_StagingDirectory
#uncomment if using Run-AlPipeline and comment line above 
#Copy-Item -Path (Join-Path $RepositoryDirectory -ChildPath("\.output\" + $app.publisher + "_" + $app.Name + "_" + $app.Version + ".app")) -Destination $env:Build_StagingDirectory
Copy-Item -Path (Join-Path $RepositoryDirectory -ChildPath("app.json")) -Destination $env:Build_StagingDirectory
Copy-Item -Path (Join-Path $env:Build_SourcesDirectory -ChildPath("Build and Release\Config\v22\*")) -Destination $env:Build_StagingDirectory -Recurse  
Copy-Item -Path (Join-Path $env:Build_SourcesDirectory -ChildPath("Build and Release\*")) -Include *.ps1  -Destination $env:Build_StagingDirectory -Recurse
#copy app to build staging directory

#sign app
write-host "##[command]Signing app"
Set-Location "C:\Program Files (x86)\Windows Kits\10\bin\10.0.22621.0\x64"
.\signtool.exe sign /f "C:\Sign\CodeSign.pfx" /p ************* /t http://timestamp.digicert.com /fd certHash (Join-Path $env:Build_StagingDirectory -ChildPath($app.publisher + "_" + $app.Name + "_" + $app.Version + ".app"))
#sign app

Write-Host "##vso[build.updatebuildnumber]$nextVersion"

Please take note of the presence of a container existence check, strategically implemented to enhance the efficiency of the build process. Additionally, it is worth highlighting the incorporation of a code signing component into the procedure.

Also, there is a piece of code that downloads dependencies from Azure Dev Ops, using PAT.

Build pipeline of your app

  • Pipelines -> New pipeline
  • Azure Repos Git
  • Select repository
  • Existing Azure Pipelines YAML file
  • Select CICD.yml

Building App

 

After every push/pull request to defined branches in CICD.yml build pipeline is triggered

and artifacts is ready to use in release pipeline


AlValidation for appsource branch

As I mentioned earlier, for the AppSource branch, we run AlValidation after the app is built.

Source code of AlValidation: 

$app = (Get-Content "app.json" -Encoding UTF8 | ConvertFrom-Json)
$settings = (Get-Content -Path (Join-Path $env:Build_SourcesDirectory -ChildPath("Build and Release\Config\v22\" + $env:ConfigFile)) -Encoding UTF8 | ConvertFrom-Json)
$settings = $settings.Apps | Where-Object { $_.id -eq $app.id }

$RepositoryDirectory = Get-Location 

$installApps = New-Object System.Collections.ArrayList

import-module $env:NAVADMINTOOL | Out-Null

$Path = Join-Path $RepositoryDirectory  -ChildPath(".alpackages\")
$AllAppFiles = Get-ChildItem -Path $Path -Filter "*.app"

foreach ($AppFile in $AllAppFiles) {
    $App = Get-NAVAppInfo -Path $AppFile.FullName
    if ($App.publisher -ne "Microsoft") {
        $installApps.Add($AppFile.FullName)
    }
}

if ($installApps.Count -gt 0) {
    foreach ($installApp in $installApps) {
        write-host "##[section]Install app" $installApp
    }
}

$previousApps = New-Object System.Collections.ArrayList
$repositoryName = $settings.repositoryName

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

Try {
    #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 previous 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 previous build id

    #download previous 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 = $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
    Expand-Archive -LiteralPath $OutFile -DestinationPath $repositoryName
    $sourcepath = Join-Path $RepositoryDirectory -ChildPath($repositoryName + "\" + $ArtifactName + "\*")
    $previousApp = Get-ChildItem -Path $sourcepath -Filter "*.app"

    $previousApp = $previousApp.FullName
    write-host "##[section]Previous app" $previousApp
    $previousApps.Add($previousApp)
    #download previous app
}
Catch {
    write-host "##[section]There is no previous app"
}

$appFile = Get-ChildItem -Path $env:Build_StagingDirectory -Filter "*.app"
$appFile = $appFile.FullName
write-host "##[section]App" $appFile

Write-Host "##[command] Starting Docker Desktop and switching to windows containers"
Set-Location 'C:\Program Files\Docker\Docker'
& '.\Docker Desktop.exe'

Set-Location "C:\Program Files\Docker\Docker"
& '.\DockerCli.exe' -SwitchWindowsEngine

Get-Process *docker*

Get-Service *docker*

Set-Location $RepositoryDirectory

Run-AlValidation -installApps @($installApps) -previousApps @($previousApps) -apps @( $appFile ) -validateCurrent -countries "hr" -affixes "eve" -supportedCountries "hr" -failOnError -throwOnError

Notice that we are downloading previous app from Azure Dev Ops using PAT.


Recommendation

Use Azure Key Vault secrets in Azure Pipelines for secrets in your pipelines, for example refresh tokens, PAT, usernames, passwords etc.

Useful blog post about it.

Conclusion

Unlike the simple PTE app in the previous posts, here is shown how the build pipeline can be adapted for a slightly more complex scenario, when there is a dependency among apps. 

 

The next post will contain a description of how to deploy apps on a self-hosted agent.


Add comment

Comments

John
a year ago

How do you handle dependent apps, and what about running tests?

Krunoslav Pipić
a year ago

It is described in this part:
#downloading dependencies

So, using PAT, dependency apps are downloaded. As for the tests, I plan to write a blog post about it. It is possible to run tests with a separate command, Run-TestsInBcContainer, or with the command Run-AlPipeline, which consolidates the entire process of building apps, publishing, signing, running tests, and validation, where a comparison is made with the previous version of the app.

$Params = @{
containerName = $ContainerName
baseFolder = $RepositoryDirectory
appFolders = "source"
testFolders = "test"
AppSourceCopMandatoryAffixes = $AppSourceCopMandatoryAffixes
AppSourceCopSupportedCountries = $AppSourceCopSupportedCountries
artifact = $artifactUrl
enableAppSourceCop = $true
enableCodeCop = $true
enableExternalRulesets = $true
rulesetFile = $rulesetFile
enableUICop = $true
installTestFramework = $true
installTestLibraries = $true
installTestRunner = $true
keepContainer = $true
reUseContainer = $true
enableTaskScheduler = $false
credential = $Credential
failOn = "warning"
codeSignCertPfxFile = $codeSignCertPfxFile
codeSignCertPfxPassword = $pfxPassword
installApps = $installApps
previousApps = $previousApps
buildArtifactFolder = $stagingDirectory
appBuild = $versionBuild
appRevision = $versionRevision
PreCompileApp = $PreCompileApp
}

Run-AlPipeline @Params

John
a year ago

It seems like you are running two agents and creating two containers? Right! How do you control which agent runs in the pipeline to make sure it is not the same agent that creates the same container twice?

KRUNOSLAV PIPIĆ
a year ago

For the purpose of building the application, the container on which the build runs is not crucial. However, if you wish to specify that the build should run on a specific agent, the demands parameter can be added to the YAML script:

Link to demands parameter documentation

Once the build is completed, artifacts are generated, which the release pipeline will pick up. Therefore, it is essential for me to specify the agent on which the release pipeline runs so that I can publish to the exact environment where the release pipeline operates.

In theory, you can have a separate agent solely for the build, and then use N agents for the release pipeline, where each agent targets a specific environment precisely.

e.g.
In release pipeline:
under demands
agent.name
equals
NavBizDev03

Charles
6 months ago

Your blog has been invaluable in getting this a build process setup for us. Thank you!

I am facing a problem though when it comes to depdencies. Right now my daily build process creates a container, but I then need to depend on an ISV app we utilize. Right now it is trying to download the dependency from the build container. Is there a way to have it installed when compiling the app or should I edit the build container script to install the apps when I am building the container daily?

Krunoslav Pipić
5 months ago

check the next post:

https://www.southfieldblog.com/1448935_business-central-release-pipeline-to-onprem-environments