RuntimePackages module
Module: businessdev.ALbuild.RuntimePackages • Tier: Licensed • Cmdlets: 7
The RuntimePackages module builds signed Business Central runtime packages for on-premise distribution — one per supported platform version — using a batched, parallel and incremental engine. See the Runtime packages deep dive. This is a licensed feature, enforced at runtime by Assert-ALbuildLicensed.
Licensing
Cmdlets in this module are part of a licensed tier and call Assert-ALbuildLicensed on entry. Without a valid ALbuild license they fail with clear remediation guidance. See Licensing & tiers.
Cmdlets in this module
| Cmdlet | Description |
|---|---|
Build-BcRuntimePackages | Builds runtime packages for one or more apps across Business Central platform versions (licensed). |
Get-BcRuntimePackageBuildPlan | Determines which Business Central platform versions still need a runtime package built. |
Get-BcRuntimeWorkSet | Plans outstanding runtime-package work, grouped by BC platform version across all products. |
Invoke-BcRuntimeFactory | Builds runtime packages for a slice of platform versions, several containers at a time. |
New-BcIndirectNuGetPackage | Creates an "indirect" NuGet package that maps platform versions to runtime packages. |
New-BcRuntimePackage | Generates a runtime package for a published app from a Business Central container. |
Publish-BcRuntimePackage | Publishes a runtime NuGet package to a feed (licensed). |
Build-BcRuntimePackages
Builds runtime packages for one or more apps across Business Central platform versions (licensed).
Implements the batched/incremental runtime-package engine: for each target platform version it creates a single container, publishes all apps into it, generates each app's runtime package and packs a runtime NuGet package (id '<publisher>.<name>.runtime-<appversion>', package version = platform version, resolvable by platform version), then removes the container. One container per version (not per app x version) minimises spin-ups; running this per version on separate agents provides the parallel dimension. A valid ALbuild license is required.
Syntax
Code
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
-App | Object[] | Yes | The apps to process. Each item: @{ AppFile; Name; Publisher; Version }. |
-PlatformVersion | String[] | Yes | Platform versions to build for (typically from Get-BcRuntimePackageBuildPlan). |
-Type | String | No | Artifact type: OnPrem (default) or Sandbox. Allowed values: OnPrem, Sandbox. Default: 'OnPrem'. |
-Country | String | No | Artifact country. Default 'w1'. Default: 'w1'. |
-Credential | PSCredential | No | Container admin credential. |
-OutputFolder | String | No | Output root for the runtime apps/packages. Default: ./runtime-output. Default: (Join-Path (Get-Location) 'runtime-output'). |
-DockerExecutable | String | No | The Docker executable to use (default 'docker'). Default: 'docker'. |
Output
PSCustomObject per produced runtime package (Version, App, RuntimeApp, NuGetPackage).
Get-BcRuntimePackageBuildPlan
Determines which Business Central platform versions still need a runtime package built.
Given the candidate platform versions, the app's minimum supported version, and the set of already-built versions, returns the versions that need building - the "incremental" part of the batched runtime-package engine. Versions below the app's minimum, or already built, are excluded.
Syntax
Code
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
-ArtifactVersion | String[] | Yes | Candidate platform versions (e.g. from Find-BcArtifactUrl ... -Select All). |
-MinimumVersion | String | No | The app's minimum supported version (typically app.json 'application'). Versions whose major is below this minimum's major are skipped. Default 0.0.0.0 (no minimum). Default: '0.0.0.0'. |
-AlreadyBuilt | String[] | No | Versions already built (present in Blob/feed) which should be skipped. |
Output
System.String - the platform versions to build, ascending.
Examples
Example 1
Code
Get-BcRuntimeWorkSet
Plans outstanding runtime-package work, grouped by BC platform version across all products.
Turns "which (product, app version, platform version) runtime packages are still missing?" into an ordered, sliceable plan. It is a pure function - no feed, blob or artifact calls - so the whole planning policy is unit-testable and a run can be inspected before a single container starts.
WHY GROUPING BY PLATFORM VERSION IS THE WHOLE POINT A runtime package is produced server-side by a running BC service tier, and Microsoft only guarantees it on the exact platform version it was produced on - so the (product x platform) matrix is irreducible. What IS reducible is the container count. Measured on build 27197, provisioning a container costs 238-564 s depending on the major, while the value-adding work per product is a constant ~80 s. Iterating platform-versions-per-product, as one pipeline per product does, pays that provisioning cost once per product; iterating products-per-platform-version pays it once, full stop. For a 12-product catalogue over ~192 versions that is ~2 300 container starts against 192.
It also removes a failure mode rather than working around it: several products depend on other products in the same catalogue (Extension License, 365 business API). Per-product pipelines had to fetch those dependencies' runtime packages from blob storage and SKIP the platform version when they were not there yet. Built in one container in dependency order, the dependency is simply present - it was built moments earlier, in the same container.
LANES A product release invalidates the entire matrix at once, and a full rebuild is hours of work no matter how it is scheduled. Customers, however, pull the newest majors first. -FastLaneMinMajor splits the plan so those versions are built and published first, and the long tail follows behind without holding the release up.
SLICES Each slice is the unit of work for one agent job. -MaxVersionsPerSlice bounds how long a single job runs, which matters because a job holds one of the organisation's few parallel slots for its whole lifetime - and because a job that dies loses only its in-flight version, not the run.
Syntax
Code
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
-Product | Object[] | Yes | The catalogue, as objects with: Name - display name (used in logs and the per-app result). AppId - app.json 'id'; keys the blob layout <appId>/<platformVersion>/<file>. AppVersion - the released version to build runtime packages for. MinimumVersion - app.json 'application'; platform versions below it are not applicable. Compared as a whole version, so a '17.1.0.0' minimum really does exclude 17.0.x. DependsOn - OPTIONAL names of other catalogue products this one needs installed first. Projects - OPTIONAL, passed through untouched for the worker. Country - OPTIONAL, passed through untouched. |
-PlatformArtifact | Object[] | Yes | Candidate platform versions as objects with PlatformVersion, ArtifactUrl and Country. Country is part of the grouping key, not decoration: a BC artifact carries ONE localisation, so a container built from the 'de' artifact cannot produce a runtime package for a product that ships against 'w1'. In the current catalogue Banking, ERiC and Sanction Screen build against 'de' and the rest against 'w1', so a platform version yields one container per country in use - still one per country instead of one per product. |
-ExistingPackage | String[] | No | Relative blob paths that already exist ('<appId>/<platformVersion>/<file>.app'). A (product, platform version) pair whose file is already there is reported as skipped rather than rebuilt, which is what makes a re-run after a failure cheap and a scheduled sweep close to free. |
-ExistingFeedPackage | String[] | No | Pairs of '<appId>/<platformVersion>' whose NuGet package is already on the feed. OPTIONAL, and it changes the meaning of 'already done': when supplied, a pair counts as done only when the blob file AND the feed package exist. Without it, idempotency rests on blob storage alone - and blob and feed can drift. The checkpoint uploads to blob first and pushes to the feed second, and a failed push is caught and warned about rather than fatal (one bad push must not lose a whole slice). The next run then sees the blob file, skips the version, and the NuGet package stays missing for good. Reversing the order would only move the hole to the other side. The version part is normalised to four parts before comparing, because NuGet normalises it away on publish: platform version 27.10.53179.0 is listed by the feed as '27.10.53179', while 28.3.52162.52222 keeps all four. Comparing the strings as they come back would match nothing for every version with a zero revision - and 'nothing on the feed' means rebuild the whole matrix. Measured on the live feed: Address Validation reports 130 three-part versions. Pass nothing when the run does not publish to a feed; the behaviour is then exactly as before. |
-SkipPlatformVersion | String[] | No | Platform versions to exclude outright - Microsoft occasionally publishes an artifact that cannot produce a working container, and one bad version must not stall the catalogue. |
-FastLaneMinMajor | Int32 | No | Platform majors >= this go into the FastLane; everything else into the LongTail. 0 (default) disables the split and puts everything in a single 'All' lane. Default: 0. |
-Lane | String | No | Which lane to return: All (default), FastLane or LongTail. Allowed values: All, FastLane, LongTail. Default: 'All'. |
-MaxVersionsPerSlice | Int32 | No | Maximum platform versions per slice. Default 12. Default: 12. |
Output
PSCustomObject with Slices (Index, Lane, Items[]) and Summary (per-product counts).
Examples
Example 1
Code
Invoke-BcRuntimeFactory
Builds runtime packages for a slice of platform versions, several containers at a time.
Runs the work items from Get-BcRuntimeWorkSet through a pool of worker threads, each owning one BC container for one platform version, and checkpoints every version as soon as it is done.
WHY A POOL INSIDE ONE JOB Throughput here is capped by the organisation's four parallel self-hosted jobs, not by the build host - which has 36 cores and 192 GB of RAM and was measured with ~72 GB still free while four containers ran. Adding pipeline stages cannot get past that cap; adding workers inside a job does, and costs nothing.
WHY CHECKPOINTING Build 27197 built 186 runtime packages and shipped 130 of them. Upload ran once, after the whole loop, so when the run was cancelled ten hours of finished work was thrown away. -OnVersionComplete runs after each platform version, so a cancelled or crashed run loses at most the version still in flight.
The callback runs on the DISPATCHER thread, deliberately. Uploading to blob storage and pushing to a feed needs an Azure PowerShell context, and those context objects are not safe to share across runspaces. Workers produce files; the dispatcher ships them.
PER-APP VISIBILITY Azure DevOps renders a stage as a single icon, and giving every app its own stage would mean a container per (app, platform version) - the arrangement the factory exists to remove. So the per-app result is carried in the artefacts a job can publish instead:
- the log, one collapsible '##[group]' per platform version with one line per app;
- -JUnitPath, one test case per app per version for PublishTestResults@2, which is what puts a green/red/skipped entry per app into the Tests tab;
- -SummaryPath, the app x version matrix for '##vso[task.uploadsummary]'.
Worker output is captured to a per-version file and replayed by the dispatcher when the version finishes, so the log reads as ordered blocks instead of interleaved lines from parallel workers.
Syntax
Code
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
-WorkItem | Object[] | Yes | Work items from Get-BcRuntimeWorkSet (PlatformVersion, ArtifactUrl, Products[]). |
-WorkRoot | String | Yes | Root for the private per-worker working copies. |
-OutputFolder | String | Yes | Root of the '<appId>/<platformVersion>/' output layout. |
-Throughput | Int32 | No | Requested worker count. Capped by available memory and disk - see Get-BcRuntimeWorkerCount. Default: 3. |
-MemoryLimit | String | No | Per-container memory limit. Default: '8G'. |
-ReserveHostGb | Int32 | No | Host memory to leave for the agent, the compiler and the OS. Default: 16. |
-Credential | PSCredential | No | Container admin credential. |
-LicenseFile | String | No | BC licence for the containers. |
-LegacyLicenseFile | String | No | The .flf license for platform majors up to 19; BC20 and newer take -LicenseFile. Both are needed because one run spans majors on either side of that change. |
-Signing | Hashtable | No | Splat for Invoke-BcAppSigning. |
-TeardownMode | String | No | Full (default) removes each app and its dependency chain after its runtime package is produced. Allowed values: Full, AppOnly. Default: 'Full'. |
-UseImageCache | switch | No | Start containers from the cached version-specific image. |
-OnVersionComplete | ScriptBlock | No | Scriptblock invoked on the dispatcher thread with the finished version result - the checkpoint. |
-LogFolder | String | No | Where per-version worker logs are written. Defaults to a folder under -WorkRoot. |
-JUnitPath | String | No | Write JUnit results here for PublishTestResults@2. |
-SummaryPath | String | No | Write the Markdown matrix here for task.uploadsummary. |
-DockerExecutable | String | No | Docker executable. Default: 'docker'. |
-NoAzureDevOpsLogging | switch | No | Suppress the '##vso[...]' logging commands. Tests set this: they run inside a real Azure DevOps job, where a logged issue would mark that job rather than the run under test. |
Output
PSCustomObject: Produced, Failed, Skipped, Results[], WorkerCount.
New-BcIndirectNuGetPackage
Creates an "indirect" NuGet package that maps platform versions to runtime packages.
Produces a dependency-only NuGet package whose dependencies are the per-platform-version runtime packages. A consumer resolving against a specific Business Central build pulls the matching runtime package through this single, stable package id.
Syntax
Code
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
-PackageId | String | Yes | The indirect package id. |
-Version | String | Yes | The indirect package version (typically the app version). |
-RuntimePackage | Object[] | Yes | The runtime package dependencies, each @{ id = ...; version = ... } (version may be a range). |
-Authors | String | No | Package authors. Default '365 business development'. Default: '365 business development'. |
-Description | String | No | Package description. |
-OutputFolder | String | No | Output folder. Default: current directory. Default: (Get-Location).Path. |
Output
System.String - the path to the created .nupkg.
New-BcRuntimePackage
Generates a runtime package for a published app from a Business Central container.
Runs Get-NAVAppRuntimePackage inside the container to produce a platform-specific runtime (.runtime.app) for an already-published app, then copies it to the host. Runtime package generation is an NST (server) operation and therefore requires the container.
Syntax
Code
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
-Name | String | Yes | Container name. |
-AppName | String | Yes | The published app's name. |
-AppPublisher | String | Yes | The app publisher (used for the output file name). |
-AppVersion | String | Yes | The app version. |
-OutputFolder | String | Yes | Host folder to copy the runtime package to. |
-ServerInstance | String | No | BC server instance. Default 'BC'. Default: 'BC'. |
-DockerExecutable | String | No | The Docker executable to use (default 'docker'). Default: 'docker'. |
Output
System.String - the path to the generated runtime .app on the host.
Publish-BcRuntimePackage
Publishes a runtime NuGet package to a feed (licensed).
Pushes a runtime (or indirect) NuGet package to a NuGet v3 feed. A valid ALbuild license is required.
Syntax
Code
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
-PackagePath | String | Yes | Path to the .nupkg. |
-Url | String | Yes | The feed's NuGet v3 service index URL. |
-ApiKey | String | Yes | API key / PAT for publishing. |
-FailOnConflict | switch | No | Throw if the version already exists. |
Examples
Example 1
Code


