Modules/businessdev.ALbuild.OnPrem/Public/Publish-BcOnPremApp.ps1
|
function Publish-BcOnPremApp { <# .SYNOPSIS Publishes an AL app to an on-premises Business Central server instance (licensed). .DESCRIPTION Publishes the .app to the given server instance using the Business Central management cmdlets, optionally synchronising and installing it. Run on a host where the BC management module is available (the BC server or a deployment agent). Requires a valid ALbuild license. .PARAMETER ServerInstance The BC server instance name. .PARAMETER AppFile Path to the .app file. .PARAMETER SkipVerification Publish without signature verification. .PARAMETER Sync Synchronise after publishing. .PARAMETER Install Install after synchronising. .PARAMETER SyncMode Add (default), Clean, Development or ForceSync. .PARAMETER Scope Global (default) or Tenant. .PARAMETER Tenant Tenant. Default 'default'. .PARAMETER Development Publish via the server's development endpoint (like VS Code) as a replaceable development extension - re-publishable without a version bump, which is what test environments need. Publishes + syncs + installs in one call, so -Install/-SkipVerification/-Scope do not apply; -SyncMode maps to the dev schema-update mode. Requires -Credential. .PARAMETER Credential BC user credential to authenticate to the development endpoint (required with -Development). .PARAMETER DevServerUrl Development service base URL, e.g. https://bcserver:7049. Optional: when omitted (and -Development is set) the endpoint is resolved from the local server's CustomSettings.config as https/http://localhost: <DeveloperServicesPort>. Provide it explicitly when the agent is not the BC server, or to target a specific hostname. #> [CmdletBinding(SupportsShouldProcess)] param( [Parameter(Mandatory)] [string] $ServerInstance, [Parameter(Mandatory)] [string] $AppFile, [switch] $SkipVerification, [switch] $Sync, [switch] $Install, [ValidateSet('Add', 'Clean', 'Development', 'ForceSync')] [string] $SyncMode = 'Add', [ValidateSet('Global', 'Tenant')] [string] $Scope = 'Global', [string] $Tenant = 'default', [switch] $Development, [pscredential] $Credential, [string] $DevServerUrl ) Assert-ALbuildLicensed -Feature 'OnPrem' if ($Development) { # Dev-endpoint publish - a replaceable development extension (re-publishable without a version bump), # for test environments. Does NOT use the management cmdlets (no Import-BcManagementShell): it POSTs # the app to the server's development endpoint, which publishes + syncs + installs in one call. The # Global/Tenant path below is untouched. if (-not $Credential) { throw 'A development (dev-endpoint) publish requires -Credential (the BC user to authenticate to the dev endpoint).' } if (-not (Test-Path -LiteralPath $AppFile)) { throw "App file not found: '$AppFile'." } $devLeaf = Split-Path -Path $AppFile -Leaf if (-not $PSCmdlet.ShouldProcess($ServerInstance, "Dev-publish $devLeaf")) { return } $schemaUpdateMode = switch ($SyncMode) { 'ForceSync' { 'forcesync' } 'Clean' { 'recreate' } default { 'synchronize' } } if ($DevServerUrl) { $devBase = $DevServerUrl.TrimEnd('/') $devSsl = $devBase -like 'https:*' } else { # The agent runs on the BC server: read the dev-services port/SSL from the instance's config. $cfgFiles = @(Get-ChildItem -Path 'C:\Program Files\Microsoft Dynamics*\*\Service\CustomSettings.config' -Recurse -ErrorAction SilentlyContinue) $cfgFile = @($cfgFiles | Where-Object { try { ([xml](Get-Content -LiteralPath $_.FullName)).SelectSingleNode("//appSettings/add[@key='ServerInstance']").value -eq $ServerInstance } catch { $false } }) | Select-Object -First 1 if (-not $cfgFile) { $cfgFile = $cfgFiles | Select-Object -First 1 } if (-not $cfgFile) { throw "Could not locate the Business Central server configuration to resolve the development endpoint. Pass -DevServerUrl (e.g. https://localhost:7049) explicitly." } [xml]$cfgXml = Get-Content -LiteralPath $cfgFile.FullName $getCfg = { param($k) $n = $cfgXml.SelectSingleNode("//appSettings/add[@key='$k']"); if ($n) { $n.value } } $devPort = & $getCfg 'DeveloperServicesPort'; if (-not $devPort) { $devPort = '7049' } $devSsl = (& $getCfg 'DeveloperServicesSSLEnabled') -eq 'true' $devBase = "$(if ($devSsl) { 'https' } else { 'http' })://localhost:$devPort" } $devUrl = "$devBase/$ServerInstance/dev/apps?SchemaUpdateMode=$schemaUpdateMode" Write-ALbuildLog "Dev-publishing '$devLeaf' to $devUrl ..." Publish-BcAppToDevEndpoint -Url $devUrl -AppFile $AppFile -Credential $Credential -IgnoreSslErrors:$devSsl Write-ALbuildLog -Level Success "Dev-published '$devLeaf' to '$ServerInstance'." return } Import-BcManagementShell # loads Publish-NAVApp / Sync-NAVApp / Install-NAVApp etc. from the BC server if (-not (Test-Path -LiteralPath $AppFile)) { throw "App file not found: '$AppFile'." } $leaf = Split-Path -Path $AppFile -Leaf if (-not $PSCmdlet.ShouldProcess($ServerInstance, "Publish $leaf")) { return } # Publish and capture the server's app record via -PassThru. We deliberately do NOT read the app # identity with 'Get-NAVAppInfo -Path' - that file-reading parameter set is not reliably available # across on-prem BC versions; the record returned by Publish-NAVApp is authoritative for # Name/Publisher/Version. When the exact Name+Publisher+Version is already published, Publish-NAVApp # writes a non-terminating error and returns nothing, which we treat as "already present". $published = Publish-NAVApp -ServerInstance $ServerInstance -Path $AppFile -Scope $Scope -SkipVerification:$SkipVerification -PassThru -ErrorAction SilentlyContinue if (-not $published) { Write-ALbuildLog -Level Information "'$leaf' is already published on '$ServerInstance' in this version; nothing to do." return } # Re-read the full tenant-specific record: adds AppId and IsInstalled, which the sync / install / # upgrade flow below needs (and which are keyed by AppId + Publisher, not just Name). $info = Get-NAVAppInfo -ServerInstance $ServerInstance -Name $published.Name -Publisher $published.Publisher -Version $published.Version -Tenant $Tenant -TenantSpecificProperties # The BC/NAV management cmdlets return AppId as a 'Microsoft.Dynamics.Nav.Apps.Types.AppId' object - # and, when the legacy module proxies calls, a *Deserialized* one - which will not bind to the [Guid] # -AppId/-Id parameter of Get/Install/Start-Upgrade/Unpublish-NAVApp ("cannot convert ... to # System.Guid"). Coerce to a real Guid via its string form (works for the native and deserialized shapes). $appId = [guid]"$($info.AppId)" Write-ALbuildLog -Level Information "Published $($info.Name) $($info.Version) (AppId $appId) to '$ServerInstance'." if (-not $Sync) { return } if ($info.IsInstalled) { Write-ALbuildLog -Level Information "$($info.Name) $($info.Version) is already installed on '$ServerInstance'; nothing to sync or install." return } Sync-NAVApp -ServerInstance $ServerInstance -Name $info.Name -Publisher $info.Publisher -Version $info.Version -Mode $SyncMode -Tenant $Tenant -Force -ErrorAction Stop if (-not $Install) { Write-ALbuildLog -Level Success "Published and synced $($info.Name) $($info.Version) on '$ServerInstance'." return } # Install vs. upgrade: if any OTHER version of this app id is already published, the freshly # published one is an upgrade - run the data upgrade, then unpublish the superseded version(s). # Otherwise it is a first-time install. $priorVersions = @(Get-NAVAppInfo -ServerInstance $ServerInstance -AppId $appId -Publisher $info.Publisher -Tenant $Tenant -TenantSpecificProperties | Where-Object { $_.Version -ne $info.Version }) if ($priorVersions.Count -eq 0) { Install-NAVApp -ServerInstance $ServerInstance -AppId $appId -Publisher $info.Publisher -Version $info.Version -Tenant $Tenant -ErrorAction Stop Write-ALbuildLog -Level Success "Installed $($info.Name) $($info.Version) on '$ServerInstance'." } else { Start-NAVAppDataUpgrade -ServerInstance $ServerInstance -AppId $appId -Publisher $info.Publisher -Version $info.Version -Tenant $Tenant -ErrorAction Stop foreach ($old in $priorVersions) { # Best-effort cleanup: the upgrade to the new version already succeeded, so a failure to # remove a superseded version must NOT fail the release. Warn and carry on. try { Unpublish-NAVApp -ServerInstance $ServerInstance -AppId $appId -Publisher $info.Publisher -Version $old.Version -ErrorAction Stop Write-ALbuildLog -Level Information "Unpublished superseded $($info.Name) $($old.Version) from '$ServerInstance'." } catch { Write-ALbuildLog -Level Warning "Could not unpublish superseded $($info.Name) $($old.Version) from '$ServerInstance': $($_.Exception.Message). Continuing." } } Write-ALbuildLog -Level Success "Upgraded $($info.Name) to $($info.Version) on '$ServerInstance'." } } |