Modules/businessdev.ALbuild.Apps/Public/Publish-BcContainerApp.ps1
|
function Publish-BcContainerApp { <# .SYNOPSIS Publishes an AL app (.app) to a Business Central container, optionally syncing/installing. .DESCRIPTION Copies the .app into the container and publishes it with the BC server management cmdlets, then optionally synchronises and installs it. Requires Windows + Docker. .PARAMETER Name Container name. .PARAMETER AppFile Path to the .app file on the host. .PARAMETER SkipVerification Publish without signature verification (for unsigned development apps). .PARAMETER Sync Synchronise the app after publishing. .PARAMETER Install Install the app after synchronising. .PARAMETER SyncMode Schema sync mode: Add (default), Clean, Development or ForceSync. .PARAMETER Scope Global (default) or Tenant. .PARAMETER ServerInstance BC server instance. Default 'BC'. .PARAMETER Tenant Tenant. Default 'default'. .PARAMETER DockerExecutable The Docker executable to use (default 'docker'). .PARAMETER OperationTimeoutSeconds Maximum seconds to allow the publish/sync/install before abandoning it and throwing. Prevents a hung operation (e.g. a deadlocked schema sync) from freezing the build. Default 600. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseUsingScopeModifierInNewRunspaces', '', Justification = 'The Start-Job scriptblock declares a param() block bound positionally via -ArgumentList; $using: is intentionally not used.')] [CmdletBinding(SupportsShouldProcess)] param( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [Alias('ContainerName')] [string] $Name, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $AppFile, [switch] $SkipVerification, [switch] $Sync, [switch] $Install, [ValidateSet('Add', 'Clean', 'Development', 'ForceSync')] [string] $SyncMode = 'Add', [ValidateSet('Global', 'Tenant')] [string] $Scope = 'Global', [string] $ServerInstance = 'BC', [string] $Tenant = 'default', [int] $OperationTimeoutSeconds = 600, [string] $DockerExecutable = 'docker' ) if (-not (Test-Path -LiteralPath $AppFile)) { throw "App file not found: '$AppFile'." } if (-not $PSCmdlet.ShouldProcess($Name, "Publish $(Split-Path $AppFile -Leaf)")) { return } $containerPath = Copy-BcFileToContainer -Name $Name -Source $AppFile -DockerExecutable $DockerExecutable $script = { $info = Get-NAVAppInfo -Path $Path # Resolve the NAV management module so each timeout-bounded child job can import it. $navMgmt = @(Get-ChildItem 'C:\Program Files\Microsoft Dynamics NAV\*\Service\Management\Microsoft.Dynamics.Nav.Management.dll' -ErrorAction SilentlyContinue) if (-not $navMgmt) { $navMgmt = @(Get-ChildItem 'C:\Program Files\Microsoft Dynamics NAV' -Recurse -Filter 'Microsoft.Dynamics.Nav.Management.dll' -ErrorAction SilentlyContinue) } $navMgmtPath = if ($navMgmt) { $navMgmt[0].FullName } else { '' } # Show a single operation's job as a growing-dots line, then 'OK' / 'FAILED <complete error>' # / 'TIMED OUT', so EACH step is shown separately and it is obvious which one hangs. The # caller builds the job with explicit scalar arguments (a robust Start-Job pattern). function Wait-BcStepJob { param([System.Management.Automation.Job] $Job, [string] $Label) $stepStart = Get-Date [System.Console]::Out.Write("$Label "); [System.Console]::Out.Flush() $elapsed = 0 while (-not (Wait-Job $Job -Timeout $PollSeconds)) { $elapsed += $PollSeconds if ($elapsed -ge $OperationTimeoutSeconds) { Stop-Job $Job -ErrorAction SilentlyContinue Remove-Job $Job -Force -ErrorAction SilentlyContinue [System.Console]::Out.WriteLine("TIMED OUT after $OperationTimeoutSeconds s"); [System.Console]::Out.Flush() # A stalled publish/sync is usually a *server-side* operation that failed (e.g. an # extension recompile) whose real error only lands in the NAV server event log - # the cmdlet just never returns. Surface that log entry so the reason is visible. $serverErr = '' try { $evt = Get-WinEvent -FilterHashtable @{ LogName = 'Application'; ProviderName = "MicrosoftDynamicsNavServer`$$ServerInstance"; Level = 2; StartTime = $stepStart } -MaxEvents 1 -ErrorAction SilentlyContinue if ($evt) { $serverErr = if ($evt.Message -match '(?s)\bMessage\b\s*(.+)$') { $matches[1] } else { $evt.Message } $serverErr = ($serverErr -replace '\x1b\[[0-9;]*m', '' -replace '\s+', ' ').Trim() } } catch { $serverErr = '' } # best-effort: never let log-reading mask the timeout if ($serverErr) { [System.Console]::Out.WriteLine("NAV server log: $serverErr"); [System.Console]::Out.Flush() return [PSCustomObject]@{ Ok = $false; Error = "$Label timed out after $OperationTimeoutSeconds s. NAV server log: $serverErr" } } return [PSCustomObject]@{ Ok = $false; Error = "$Label timed out after $OperationTimeoutSeconds s (operation hung)" } } [System.Console]::Out.Write('. '); [System.Console]::Out.Flush() } try { # The job emits warnings as 'ALBUILD-WARN:' data lines (a child job's warnings are # written straight to the host, so they cannot be redirected from the parent). Print # 'OK' on the step line, then each warning on its own line; the rest is real output. $results = Receive-Job $Job -ErrorAction Stop $jobWarn = @($results | Where-Object { "$_" -like 'ALBUILD-WARN:*' } | ForEach-Object { "$_" -replace '^ALBUILD-WARN:', '' }) $out = @($results | Where-Object { "$_" -notlike 'ALBUILD-WARN:*' }) [System.Console]::Out.WriteLine('OK'); [System.Console]::Out.Flush() foreach ($w in $jobWarn) { [System.Console]::Out.WriteLine(" WARNING: $(($w -replace '\s+', ' ').Trim())"); [System.Console]::Out.Flush() } $txt = if ($out) { ($out | Out-String).Trim() } else { '' } if ($txt) { [System.Console]::Out.WriteLine($txt); [System.Console]::Out.Flush() } return [PSCustomObject]@{ Ok = $true; Error = '' } } catch { [System.Console]::Out.WriteLine('FAILED'); [System.Console]::Out.Flush() # Show the COMPLETE error (full compiler/server output), only ANSI-stripped. $full = ("$($_.Exception.Message)" -replace '\x1b\[[0-9;]*m', '').TrimEnd() [System.Console]::Out.WriteLine($full); [System.Console]::Out.Flush() $first = @(($full -split "`r?`n") | Where-Object { $_.Trim() }) $first = if ($first.Count -gt 0) { $first[0].Trim() } else { 'failed' } return [PSCustomObject]@{ Ok = $false; Error = "$Label failed: $first" } } finally { Remove-Job $Job -Force -ErrorAction SilentlyContinue } } $app = "'$($info.Name) $($info.Version)'" $job = Start-Job -ScriptBlock { param($module, $si, $path, $scope, $skip) if ($module) { Import-Module $module -DisableNameChecking -ErrorAction Stop } $wv = @() Publish-NAVApp -ServerInstance $si -Path $path -Scope $scope -SkipVerification:$skip -ErrorAction Stop -WarningVariable +wv -WarningAction SilentlyContinue foreach ($x in $wv) { "ALBUILD-WARN:$x" } } -ArgumentList $navMgmtPath, $ServerInstance, $Path, $Scope, ([bool]$SkipVerification) $r = Wait-BcStepJob -Job $job -Label "Publishing $app" if (-not $r.Ok) { [System.Console]::Out.WriteLine("ALBUILD-ERROR:$($r.Error)"); [System.Console]::Out.Flush(); exit 1 } if ($DoSync) { $job = Start-Job -ScriptBlock { param($module, $si, $name, $ver, $mode, $tenant) if ($module) { Import-Module $module -DisableNameChecking -ErrorAction Stop } $wv = @() Sync-NAVApp -ServerInstance $si -Name $name -Version $ver -Mode $mode -Tenant $tenant -Force -ErrorAction Stop -WarningVariable +wv -WarningAction SilentlyContinue foreach ($x in $wv) { "ALBUILD-WARN:$x" } } -ArgumentList $navMgmtPath, $ServerInstance, $info.Name, $info.Version, $Mode, $Tenant $r = Wait-BcStepJob -Job $job -Label "Syncing $app" if (-not $r.Ok) { [System.Console]::Out.WriteLine("ALBUILD-ERROR:$($r.Error)"); [System.Console]::Out.Flush(); exit 1 } } if ($DoInstall) { $job = Start-Job -ScriptBlock { param($module, $si, $name, $ver, $tenant) if ($module) { Import-Module $module -DisableNameChecking -ErrorAction Stop } $wv = @() Install-NAVApp -ServerInstance $si -Name $name -Version $ver -Tenant $tenant -ErrorAction Stop -WarningVariable +wv -WarningAction SilentlyContinue foreach ($x in $wv) { "ALBUILD-WARN:$x" } } -ArgumentList $navMgmtPath, $ServerInstance, $info.Name, $info.Version, $Tenant $r = Wait-BcStepJob -Job $job -Label "Installing $app" if (-not $r.Ok) { [System.Console]::Out.WriteLine("ALBUILD-ERROR:$($r.Error)"); [System.Console]::Out.Flush(); exit 1 } } [System.Console]::Out.WriteLine("Published $($info.Name) $($info.Version)") } # Stream the in-container output so the progress line is visible live; the full output is still # returned. Log a concise host-side summary (the per-app line already streamed). $null = Invoke-BcContainerCommand -ContainerName $Name -ScriptBlock $script -DockerExecutable $DockerExecutable -StreamOutput -Variables @{ Path = $containerPath; ServerInstance = $ServerInstance; Scope = $Scope SkipVerification = [bool]$SkipVerification; DoSync = [bool]$Sync; DoInstall = [bool]$Install; Mode = $SyncMode; Tenant = $Tenant OperationTimeoutSeconds = [int]$OperationTimeoutSeconds; PollSeconds = 10 } Write-ALbuildLog -Level Success "Published & installed '$(Split-Path -Path $AppFile -Leaf)' into '$Name'." } |