From 16c5653edfdd58156aa859ca07f19bcfc6366f3e Mon Sep 17 00:00:00 2001 From: aholstrup1 Date: Tue, 7 Jul 2026 13:15:24 +0200 Subject: [PATCH 1/2] Support trackALAlertsInGitHub for workspace compilation build path Thread an ErrorLogDirectory through Build-AppsInWorkspace and CompileAppsInWorkspace so that, when trackALAlertsInGitHub is enabled with workspaceCompilation, altool workspace compile writes per-project *.errorLog.json diagnostics into .buildartifacts/ErrorLogs (the same folder the classic Run-AlPipeline path uses). Defensively probes 'workspace compile --help' and only passes --errorlogdirectory when supported. Adds Pester coverage. AB#641509 --- Actions/.Modules/CompileFromWorkspace.psm1 | 51 +++++++++++++++++- Actions/CompileApps/Compile.ps1 | 11 ++++ RELEASENOTES.md | 4 ++ Tests/CompileFromWorkspace.Test.ps1 | 63 ++++++++++++++++++++++ 4 files changed, 128 insertions(+), 1 deletion(-) diff --git a/Actions/.Modules/CompileFromWorkspace.psm1 b/Actions/.Modules/CompileFromWorkspace.psm1 index 8fa9c086d0..518b5f426a 100644 --- a/Actions/.Modules/CompileFromWorkspace.psm1 +++ b/Actions/.Modules/CompileFromWorkspace.psm1 @@ -243,6 +243,8 @@ function Get-ALTool { Path to the output folder for compiled .app files. Defaults to PackageCachePath. .PARAMETER LogDirectory Path to the directory for compilation log files. +.PARAMETER ErrorLogDirectory + Path to the directory where per-project error log files (SARIF-style diagnostics) are written. When set (and supported by the compiler), each project's alc invocation produces a '_.errorLog.json' file used to surface AL alerts in GitHub. .PARAMETER MaxCpuCount Maximum number of parallel compilation processes. Defaults to 1. .PARAMETER AssemblyProbingPaths @@ -287,6 +289,8 @@ function Build-AppsInWorkspace { [string]$OutFolder, [Parameter(Mandatory = $false)] [string]$LogDirectory, + [Parameter(Mandatory = $false)] + [string]$ErrorLogDirectory, # Optional parameters [Parameter(Mandatory = $false)] [int]$MaxCpuCount = 1, @@ -359,6 +363,7 @@ function Build-AppsInWorkspace { PackageCachePath = $PackageCachePath OutFolder = $OutputFolder LogDirectory = $LogDirectory + ErrorLogDirectory = $ErrorLogDirectory AssemblyProbingPaths = $AssemblyProbingPaths Analyzers = $Analyzers CustomAnalyzers = $CustomAnalyzers @@ -445,11 +450,40 @@ function Copy-CompiledAppsToOutput { return $generatedAppFiles } -function CompileAppsInWorkspace { +<# + .SYNOPSIS + Determines whether the AL tool's 'workspace compile' command supports a given option. + .DESCRIPTION + Probes 'altool workspace compile --help' and checks whether the specified option name appears in the output. + Used to remain compatible with compiler versions that predate newly introduced options. + .PARAMETER ALToolPath + Path to the AL tool executable (altool). + .PARAMETER Option + The option name to look for (without leading dashes), e.g. 'errorlogdirectory'. + .OUTPUTS + Boolean indicating whether the option is supported. +#> +function Test-ALToolWorkspaceCompileSupportsOption { param( [Parameter(Mandatory = $true)] [string]$ALToolPath, + [Parameter(Mandatory = $true)] + [string]$Option + ) + + try { + $compileHelp = & $ALToolPath workspace compile --help 2>&1 | Out-String + return ($compileHelp -match [regex]::Escape($Option)) + } catch { + OutputDebug -message "Failed to probe altool workspace compile --help for option '$Option': $_" + return $false + } +} +function CompileAppsInWorkspace { + param( + [Parameter(Mandatory = $true)] + [string]$ALToolPath, [Parameter(Mandatory = $true)] [string]$WorkspaceFile, @@ -499,6 +533,9 @@ function CompileAppsInWorkspace { [Parameter(Mandatory = $false)] [string]$LogDirectory, + [Parameter(Mandatory = $false)] + [string]$ErrorLogDirectory, + [Parameter(Mandatory = $false)] [string]$OutFolder ) @@ -602,6 +639,18 @@ function CompileAppsInWorkspace { $arguments += $defaultLogDir } + if ($ErrorLogDirectory) { + # The --errorlogdirectory option emits one '_.errorLog.json' per project, + # which ProcessALCodeAnalysisLogs consumes to surface AL alerts in GitHub. + # It may not exist in the consumed compiler version yet, so probe --help before using it. + if (Test-ALToolWorkspaceCompileSupportsOption -ALToolPath $ALToolPath -Option 'errorlogdirectory') { + $arguments += "--errorlogdirectory" + $arguments += $ErrorLogDirectory + } else { + OutputWarning "--errorlogdirectory is not supported by this compiler version and will be ignored. AL code alerts will not be generated for the workspace compilation build." + } + } + $generatedAppFiles = @() $originalEncoding = [Console]::OutputEncoding try { diff --git a/Actions/CompileApps/Compile.ps1 b/Actions/CompileApps/Compile.ps1 index 8874d89e2d..5b7b13984b 100644 --- a/Actions/CompileApps/Compile.ps1 +++ b/Actions/CompileApps/Compile.ps1 @@ -252,6 +252,17 @@ try { $allAnalyzers = @(Get-CodeAnalyzers -Settings $settings) $allCustomAnalyzers = @(Get-CustomAnalyzers -Settings $settings -CompilerFolder $compilerFolder) + # When AL alert tracking is enabled, direct per-project error logs to the same folder the classic + # Run-AlPipeline path uses (.buildartifacts/ErrorLogs), so ProcessALCodeAnalysisLogs and the + # ErrorLogs artifact-publish step pick them up unchanged. + if ($settings.trackALAlertsInGitHub) { + $errorLogsFolder = Join-Path $buildArtifactFolder "ErrorLogs" + if (-not (Test-Path $errorLogsFolder)) { + New-Item $errorLogsFolder -ItemType Directory -Force | Out-Null + } + $buildParams.ErrorLogDirectory = $errorLogsFolder + } + # Start compilation - only compile folders that need building (all in full build, modified-only in incremental) $appFiles = @() $testAppFiles = @() diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 855c42c428..1327428685 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -16,6 +16,10 @@ Workspace compilation now finds altool both in the platform-specific subfolder ( `ProcessALCodeAnalysisLogs` now URI-encodes each segment of the artifact location path when writing SARIF (for example `1.Setup Data/Foo.al` becomes `1.Setup%20Data/Foo.al`). Paths that contain spaces or other characters that are not valid in a URI previously caused `github/codeql-action/upload-sarif` to log "is not a valid URI" warnings and could prevent AL code scanning alerts from mapping to the correct files. The `/` path separators are preserved so the path structure is unchanged. +### AL alerts for the workspace compilation build + +The `trackALAlertsInGitHub` setting now also works when `workspaceCompilation` (preview) is enabled. When both are turned on, AL-Go passes `--errorlogdirectory` to `altool workspace compile` so each project emits an `*.errorLog.json` diagnostics file into `.buildartifacts/ErrorLogs/`, which is processed into SARIF and surfaced as code scanning alerts — matching the classic Run-AlPipeline behavior. If the consumed compiler version does not yet support `--errorlogdirectory`, the option is skipped and a warning is logged (the rest of the build is unaffected). + ## v9.1 ### Resilient Pull Request Status Check for large builds diff --git a/Tests/CompileFromWorkspace.Test.ps1 b/Tests/CompileFromWorkspace.Test.ps1 index c4b264d1fd..27e6d2a620 100644 --- a/Tests/CompileFromWorkspace.Test.ps1 +++ b/Tests/CompileFromWorkspace.Test.ps1 @@ -1302,6 +1302,69 @@ Write-Host "Post-compile: $($appFiles.Count) apps" $script:capturedArguments | Should -Contain '--logdirectory' } } + + It 'Includes --errorlogdirectory when ErrorLogDirectory is set and the compiler supports it' { + InModuleScope CompileFromWorkspace { + $script:capturedArguments = @() + $wsFile = Join-Path $TestDrive 'test.code-workspace' + Set-Content -Path $wsFile -Value '{}' + $outDir = Join-Path $TestDrive 'out-args-errorlog1' + New-Item -Path $outDir -ItemType Directory -Force | Out-Null + $errorLogDir = Join-Path $TestDrive 'ErrorLogs' + Mock RunAndCheck { + $script:capturedArguments = $args + } + Mock Copy-CompiledAppsToOutput { return @() } + # Simulate a compiler that advertises the --errorlogdirectory option in its help + Mock Test-ALToolWorkspaceCompileSupportsOption { return $true } + + CompileAppsInWorkspace -ALToolPath 'altool.exe' -WorkspaceFile $wsFile -MaxCpuCount 1 -OutFolder $outDir -PackageCachePath $outDir -ErrorLogDirectory $errorLogDir + + $script:capturedArguments | Should -Contain '--errorlogdirectory' + $script:capturedArguments | Should -Contain $errorLogDir + } + } + + It 'Omits --errorlogdirectory and warns when the compiler does not support it' { + InModuleScope CompileFromWorkspace { + $script:capturedArguments = @() + $wsFile = Join-Path $TestDrive 'test.code-workspace' + Set-Content -Path $wsFile -Value '{}' + $outDir = Join-Path $TestDrive 'out-args-errorlog2' + New-Item -Path $outDir -ItemType Directory -Force | Out-Null + $errorLogDir = Join-Path $TestDrive 'ErrorLogs' + Mock RunAndCheck { + $script:capturedArguments = $args + } + Mock Copy-CompiledAppsToOutput { return @() } + Mock OutputWarning {} + # Simulate a compiler whose help does not mention the option + Mock Test-ALToolWorkspaceCompileSupportsOption { return $false } + + CompileAppsInWorkspace -ALToolPath 'altool.exe' -WorkspaceFile $wsFile -MaxCpuCount 1 -OutFolder $outDir -PackageCachePath $outDir -ErrorLogDirectory $errorLogDir + + $script:capturedArguments | Should -Not -Contain '--errorlogdirectory' + Should -Invoke OutputWarning -Times 1 + } + } + + It 'Omits --errorlogdirectory when ErrorLogDirectory is not set' { + InModuleScope CompileFromWorkspace { + $script:capturedArguments = @() + $wsFile = Join-Path $TestDrive 'test.code-workspace' + Set-Content -Path $wsFile -Value '{}' + $outDir = Join-Path $TestDrive 'out-args-errorlog3' + New-Item -Path $outDir -ItemType Directory -Force | Out-Null + Mock RunAndCheck { + $script:capturedArguments = $args + } + Mock Copy-CompiledAppsToOutput { return @() } + + CompileAppsInWorkspace -ALToolPath 'altool.exe' -WorkspaceFile $wsFile -MaxCpuCount 1 -OutFolder $outDir -PackageCachePath $outDir + + $script:capturedArguments | Should -Not -Contain '--errorlogdirectory' + } + } } Describe 'New-AppSourceCopJson' { From 6debd3c4aa5b46f85b55c5db0279a1746df9cb17 Mon Sep 17 00:00:00 2001 From: aholstrup1 Date: Mon, 3 Aug 2026 13:38:40 +0200 Subject: [PATCH 2/2] Check altool exit code when probing workspace compile options Test-ALToolWorkspaceCompileSupportsOption ran 'altool workspace compile --help' but never checked \0. A native executable does not throw on a non-zero exit, so a failed probe whose output happened to contain the option name would be treated as supported and the subsequent compile could hard-fail instead of taking the warn-and-skip fallback. Now returns false on any non-zero exit. Adds direct Pester coverage (supported output, unsupported output, and failed probe) via a controllable fake altool script. --- Actions/.Modules/CompileFromWorkspace.psm1 | 7 ++++ Tests/CompileFromWorkspace.Test.ps1 | 47 ++++++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/Actions/.Modules/CompileFromWorkspace.psm1 b/Actions/.Modules/CompileFromWorkspace.psm1 index 518b5f426a..4e667dafb6 100644 --- a/Actions/.Modules/CompileFromWorkspace.psm1 +++ b/Actions/.Modules/CompileFromWorkspace.psm1 @@ -473,6 +473,13 @@ function Test-ALToolWorkspaceCompileSupportsOption { try { $compileHelp = & $ALToolPath workspace compile --help 2>&1 | Out-String + # A native executable does not throw merely because it exits non-zero, so the exit code must be + # checked explicitly. If the probe failed, treat the option as unsupported so the caller takes the + # promised warn-and-skip fallback instead of parsing error/usage output as a positive match. + if ($LASTEXITCODE -ne 0) { + OutputDebug -message "Probing altool workspace compile --help for option '$Option' returned exit code $LASTEXITCODE; treating the option as unsupported." + return $false + } return ($compileHelp -match [regex]::Escape($Option)) } catch { OutputDebug -message "Failed to probe altool workspace compile --help for option '$Option': $_" diff --git a/Tests/CompileFromWorkspace.Test.ps1 b/Tests/CompileFromWorkspace.Test.ps1 index 27e6d2a620..6742024788 100644 --- a/Tests/CompileFromWorkspace.Test.ps1 +++ b/Tests/CompileFromWorkspace.Test.ps1 @@ -1367,6 +1367,53 @@ Write-Host "Post-compile: $($appFiles.Count) apps" } } + Describe 'Test-ALToolWorkspaceCompileSupportsOption' { + BeforeAll { + # The probe invokes '& $ALToolPath workspace compile --help'. We stand in a fake altool as a + # .ps1 script (invoked via the call operator on both PS5 and PS7) whose output and exit code we + # control per test, so the real help-matching and exit-code handling are exercised - not mocked. + $script:fakeAltool = Join-Path $TestDrive "fake-altool.ps1" + } + + It 'Returns true when the option appears in the help output and the probe succeeds' { + Set-Content -Path $script:fakeAltool -Value @' +Write-Output "Usage: altool workspace compile [options]" +Write-Output " --errorlogdirectory Write diagnostics to " +exit 0 +'@ + InModuleScope CompileFromWorkspace -Parameters @{ altool = $script:fakeAltool } { + param($altool) + Test-ALToolWorkspaceCompileSupportsOption -ALToolPath $altool -Option 'errorlogdirectory' | Should -BeTrue + } + } + + It 'Returns false when the option is absent from the help output' { + Set-Content -Path $script:fakeAltool -Value @' +Write-Output "Usage: altool workspace compile [options]" +Write-Output " --outfolder Output folder" +exit 0 +'@ + InModuleScope CompileFromWorkspace -Parameters @{ altool = $script:fakeAltool } { + param($altool) + Test-ALToolWorkspaceCompileSupportsOption -ALToolPath $altool -Option 'errorlogdirectory' | Should -BeFalse + } + } + + It 'Returns false when the probe fails (non-zero exit) even if the output contains the option' { + # An older altool may emit usage text mentioning the option while still failing. A non-zero exit + # must win so the caller falls back to warn-and-skip rather than passing an unsupported argument. + Set-Content -Path $script:fakeAltool -Value @' +Write-Output "error: unknown command 'workspace'" +Write-Output "did you mean --errorlogdirectory?" +exit 1 +'@ + InModuleScope CompileFromWorkspace -Parameters @{ altool = $script:fakeAltool } { + param($altool) + Test-ALToolWorkspaceCompileSupportsOption -ALToolPath $altool -Option 'errorlogdirectory' | Should -BeFalse + } + } + } + Describe 'New-AppSourceCopJson' { BeforeEach { # Create app folders with app.json