Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
de9d2ca
Add separate RunTests action gated by useSeparateTestAction
spetersenms Aug 6, 2026
6550465
Documentation cleanup and skip action when relevant.
spetersenms Aug 6, 2026
d375a8b
Merge branch 'main' into spetersen/separateTestAction
spetersenms Aug 7, 2026
cd2c03a
Fix failing tests
spetersenms Aug 7, 2026
d3b320f
Merge branch 'main' into spetersen/separateTestAction
spetersenms Aug 7, 2026
b015e22
Use AlTool as the default test runner in the RunTests action
spetersenms Aug 7, 2026
3ab5c81
Remove unsupported altool --testplan batching from RunTests
spetersenms Aug 10, 2026
262babf
Handle array in PS5
spetersenms Aug 10, 2026
12f6088
respect additionalCountries setting and disabledTests
spetersenms Aug 17, 2026
c9c3fbd
Run AlTool as native command to correctly handle terminating errors.
spetersenms Aug 17, 2026
a37d187
Filter out BCPT tests.
spetersenms Aug 17, 2026
07af75e
Re-run only missing results.
spetersenms Aug 17, 2026
46c8b8c
Copy TestResults.xml to artifacts folder.
spetersenms Aug 17, 2026
4d3ae9a
Export event logs from container.
spetersenms Aug 19, 2026
2f2f009
Cleanup documentation
spetersenms Aug 19, 2026
3885bca
Use AlTool batching feature and cleanup implementation.
spetersenms Aug 19, 2026
499eb15
Pre install Altool before Al test runner is invoked. Simplify test va…
spetersenms Aug 20, 2026
cd26ae8
Additional error handling and function documentation.
spetersenms Aug 20, 2026
85dbd27
Simplified Invoke-AlNativeCommand
spetersenms Aug 21, 2026
dcf9461
Simplified AlTool output parsing.
spetersenms Aug 21, 2026
5e6300b
Simplified parse check
spetersenms Aug 21, 2026
e64e58d
Write warning on eventlog capture failure instead of throwing
spetersenms Aug 24, 2026
5bf9179
Return App Path, Id and Name from Get-TestAppsToRun function.
spetersenms Aug 24, 2026
515c8fa
Cleanup of old snippets, gates and unused params
spetersenms Aug 25, 2026
723d846
Removed RunPipelineTests
spetersenms Aug 25, 2026
3d169c2
Merge branch 'main' into spetersen/separateTestAction
spetersenms Aug 25, 2026
cdb9641
Settings schema default value added. Installing AlTool to user folder…
spetersenms Aug 25, 2026
656791c
Merge branch 'spetersen-microsoft-resolve-test-action-merge' into spe…
spetersenms Aug 25, 2026
36ee6b9
Merge main
spetersenms Aug 25, 2026
55141fb
Remove New-AlToolProject as it is no longer required.
spetersenms Aug 25, 2026
c1c775f
Parse codeunits as Ints
spetersenms Aug 25, 2026
b733a94
Support test types
spetersenms Aug 25, 2026
da8f233
Merge branch 'main' into spetersen/separateTestAction
spetersenms Aug 27, 2026
f3e0106
Merge branch 'main' into spetersen/separateTestAction
spetersenms Aug 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Actions/.Modules/ReadSettings.psm1
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ function GetDefaultSettings
"doNotBuildTests" = $false
"doNotPerformUpgrade" = $false
"doNotRunTests" = $false
"useSeparateTestAction" = $false
"doNotRunBcptTests" = $false
"doNotRunPageScriptingTests" = $false
"doNotPublishApps" = $false
Expand Down
4 changes: 4 additions & 0 deletions Actions/.Modules/settings.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,10 @@
"doNotRunTests": {
"type": "boolean"
},
"useSeparateTestAction": {
"type": "boolean",
"description": "PREVIEW: When set to true, normal tests (testFolders) are no longer executed inside the RunPipeline action. Instead, RunPipeline keeps the build container alive and a separate RunTests action executes the tests afterwards. See https://aka.ms/ALGoSettings#useSeparateTestAction"
},
Comment thread
spetersenms marked this conversation as resolved.
"doNotRunBcptTests": {
"type": "boolean"
},
Expand Down
47 changes: 47 additions & 0 deletions Actions/RunPipeline/RunPipeline.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,22 @@ Param(
[string] $previousAppsPath = ''
)

function New-KeepAliveContainerCredential {
Comment thread
spetersenms marked this conversation as resolved.
<#
.SYNOPSIS
Generates a credential used to create a build container that is kept alive for the RunTests action.
.DESCRIPTION
When useSeparateTestAction is enabled, RunPipeline keeps the build container alive so the RunTests
action can run tests against it. BcContainerHelper requires an explicit credential when a container
is kept (otherwise it is created with a random password that cannot be reused). This function returns
a PSCredential with a randomly generated complex password.
#>
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '', Justification = 'A container password must be generated as plain text to build a reusable credential')]
param()
$password = "Pass!$([GUID]::NewGuid().ToString())"
return (New-Object pscredential 'admin', (ConvertTo-SecureString -String $password -AsPlainText -Force))
}

$containerBaseFolder = $null
$projectPath = $null

Expand Down Expand Up @@ -473,6 +489,36 @@ try {
$runAlPipelineParams["preprocessorsymbols"] = $settings.preprocessorSymbols
$runAlPipelineParams["features"] = $settings.features

# When useSeparateTestAction is enabled, the normal tests (testFolders) are run by the separate
# RunTests action instead of here, and the build container is kept alive for it. BCPT and page
# scripting tests are unaffected. This needs a build container, which only exists when apps are
# published (doNotPublishApps not set) and the build does not target an online environment;
# otherwise the tests are run here as usual.
$createsTestContainer = (-not $settings.doNotPublishApps) -and -not ($authContext -and $environmentName)
$keepContainerForSeparateTestAction = $false
if ($settings.useSeparateTestAction -and $createsTestContainer) {
Write-Host "useSeparateTestAction is enabled: skipping normal test execution in RunPipeline and keeping the container alive for the RunTests action"
$runAlPipelineParams["doNotRunTests"] = $true
$keepContainerForSeparateTestAction = $true

# A kept-alive container needs an explicit credential so the RunTests action can reconnect to it.
# Generate one, pass it to Run-AlPipeline, and surface it (masked, base64 JSON) via containerCredential.
if (-not $runAlPipelineParams.ContainsKey('credential')) {
$containerCredential = New-KeepAliveContainerCredential
$runAlPipelineParams["credential"] = $containerCredential

$containerCredentialPassword = $containerCredential.GetNetworkCredential().Password
$containerCredentialJson = @{ "username" = $containerCredential.UserName; "password" = $containerCredentialPassword } | ConvertTo-Json -Compress
$containerCredentialBase64 = [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($containerCredentialJson))
Write-Host "::add-mask::$containerCredentialPassword"
Write-Host "::add-mask::$containerCredentialBase64"
Add-Content -Encoding UTF8 -Path $env:GITHUB_ENV -Value "containerCredential=$containerCredentialBase64"
}
}
elseif ($settings.useSeparateTestAction) {
Write-Host "::Notice::useSeparateTestAction is enabled, but no build container is created for this project (doNotPublishApps is set or the build targets an online environment), so the RunTests action has no container to run tests against and will be skipped."
}

Write-Host "Invoke Run-AlPipeline with buildmode $buildMode"
Run-AlPipeline @runAlPipelineParams `
-accept_insiderEula `
Expand Down Expand Up @@ -518,6 +564,7 @@ try {
-pageScriptingTestResultsFolder (Join-Path $buildArtifactFolder 'PageScriptingTestResultDetails') `
-CreateRuntimePackages:$CreateRuntimePackages `
-appVersion ($versionNumber.MajorMinorVersion) -appBuild ($versionNumber.BuildNumber) -appRevision ($versionNumber.RevisionNumber) `
-keepContainer:$keepContainerForSeparateTestAction `
-uninstallRemovedApps

if ($containerBaseFolder) {
Expand Down
28 changes: 28 additions & 0 deletions Actions/RunTests/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Run tests

Run the normal tests (testFolders) for an AL-Go project against the build container created and kept alive by the RunPipeline action.

This action only does anything when the `useSeparateTestAction` setting is enabled. In that case, the RunPipeline action compiles, publishes and installs the apps, skips the normal tests and keeps the build container alive. This action then runs the normal tests against that same container and writes the results to `TestResults.xml` in the project folder.

Only normal tests (testFolders) are handled here. BCPT and page scripting tests continue to be executed by the RunPipeline action.

## INPUT

### ENV variables

| Name | Description |
| :-- | :-- |
| Settings | env.Settings must be set by a prior call to the ReadSettings Action |
| containerName | env.containerName is set by the RunPipeline action and identifies the container to run tests against (the container name is otherwise derived from the project) |

### Parameters

| Name | Required | Description | Default value |
| :-- | :-: | :-- | :-- |
| shell | | The shell (powershell or pwsh) in which the PowerShell script should run | powershell |
| project | | Project folder | '.' |
| installTestAppsJson | | Path to a JSON file containing a list of test apps to run tests in | '' |

## OUTPUT

None
97 changes: 97 additions & 0 deletions Actions/RunTests/RunTests.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
Param(
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'token', Justification = 'Exposed as $env:_token via action.yaml so downstream test override scripts (e.g. BCApps test tolerance artifact download) can authenticate.')]
[Parameter(HelpMessage = "The GitHub token running the action", Mandatory = $false)]
[string] $token,
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
[Parameter(HelpMessage = "Project folder", Mandatory = $false)]
[string] $project = "",
[Parameter(HelpMessage = "A path to a JSON-formatted list of test apps to run tests in", Mandatory = $false)]
[string] $installTestAppsJson = ''
)

<#
.SYNOPSIS
Runs the normal tests (testFolders) for an AL-Go project against the build container
created and kept alive by the RunPipeline action.
.DESCRIPTION
Runs the normal tests (testFolders) of an AL-Go project. Tests are only run when the
useSeparateTestAction setting is enabled; otherwise this action does nothing and the tests are
run by the RunPipeline action. When enabled, RunPipeline compiles, publishes and installs the
apps and keeps the build container alive, and this action runs the normal tests against that
container and writes the results to TestResults.xml in the project folder.

Only normal tests (testFolders) are run here. BCPT and page scripting tests are run by the
RunPipeline action.
.PARAMETER token
The GitHub token running the action. It is exposed as the _token environment variable by
action.yaml so downstream test override scripts (for example, BCApps test tolerance, which
downloads the unstable-tests artifact) can authenticate against GitHub.
.PARAMETER project
Project folder.
.PARAMETER installTestAppsJson
A path to a JSON-formatted list of test apps (produced by previous jobs) to run tests in.
.EXAMPLE
RunTests.ps1 -project 'MyProject'
#>

. (Join-Path -Path $PSScriptRoot -ChildPath "..\AL-Go-Helper.ps1" -Resolve)
Import-Module (Join-Path $PSScriptRoot '..\TelemetryHelper.psm1' -Resolve)
Import-Module (Join-Path $PSScriptRoot 'RunTests.psm1' -Resolve) -DisableNameChecking -Force
DownloadAndImportBcContainerHelper

function Get-TestRunnerCredential {
<#
.SYNOPSIS
Returns the credential used by the test runner to connect to the build container.
.DESCRIPTION
RunPipeline creates the container and keeps it alive when useSeparateTestAction is set.
When RunPipeline surfaces the container credential (masked, as base64-encoded JSON in
the containerCredential environment variable), it is used here so the test runner can
connect to the same container. Otherwise a default credential is used.
#>
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '', Justification = 'The container credential is surfaced by RunPipeline as plain text')]
param()
if ($ENV:containerCredential) {
$credentialJson = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($ENV:containerCredential)) | ConvertFrom-Json
$securePassword = ConvertTo-SecureString -String $credentialJson.password -AsPlainText -Force
return New-Object System.Management.Automation.PSCredential($credentialJson.username, $securePassword)
}
$securePassword = ConvertTo-SecureString -String ([GUID]::NewGuid().ToString()) -AsPlainText -Force
return New-Object System.Management.Automation.PSCredential("admin", $securePassword)
}

if ($project -eq ".") { $project = "" }

$baseFolder = $ENV:GITHUB_WORKSPACE
$projectPath = Join-Path $baseFolder $project

Write-Host "Use settings"
$settings = $env:Settings | ConvertFrom-Json | ConvertTo-HashTable

# Tests only run here when useSeparateTestAction is enabled; otherwise RunPipeline runs them.
if (-not $settings.useSeparateTestAction) {
Write-Host "useSeparateTestAction is not enabled. Tests are executed by the RunPipeline action. Skipping."
return
}

# Analyze the repository to determine the test folders (and other test related settings)
$settings = AnalyzeRepo -settings $settings -baseFolder $baseFolder -project $project -doNotCheckArtifactSetting

# Resolve the container kept alive by RunPipeline (name is deterministic per project, also exported to the environment).
$containerName = $ENV:containerName
if (-not $containerName) {
$containerName = GetContainerName($project)
}

# Credentials used to connect to the build container.
$credential = Get-TestRunnerCredential

# A RunTestsInBcContainer override script, if present, replaces the built-in BcContainerHelper test runner.
$overrideParams = Get-ScriptOverrides -ALGoFolderName (Join-Path $projectPath ".AL-Go") -OverrideScriptNames @("RunTestsInBcContainer")

Invoke-AlGoTestRun `
-settings $settings `
-projectPath $projectPath `
-containerName $containerName `
-credential $credential `
-installTestAppsJson $installTestAppsJson `
-runTestsOverride $overrideParams['RunTestsInBcContainer']
155 changes: 155 additions & 0 deletions Actions/RunTests/RunTests.psm1
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
<#
.SYNOPSIS
Helper module for the RunTests action.
.DESCRIPTION
Contains the logic for running the normal tests (testFolders) of an AL-Go project against a
build container that was created and kept alive by the RunPipeline action. Kept in a module
so the logic can be unit tested independently of the action entry script.
#>

function Get-TestAppsToRun {
<#
.SYNOPSIS
Determines the set of test app files to run tests in.
.DESCRIPTION
Collects the test apps compiled for the project (found in the build artifacts TestApps
folder) and, when runTestsInAllInstalledTestApps is enabled, the test apps installed from
previous jobs (listed in installTestAppsJson). Test apps wrapped in parentheses are
unwrapped (matching Run-AlPipeline semantics where such apps are otherwise not tested).
.PARAMETER settings
The (analyzed) AL-Go settings hashtable.
.PARAMETER projectPath
The full path to the project folder.
.PARAMETER installTestAppsJson
Path to a JSON file with the list of installed test apps.
#>
Param(
[hashtable] $settings,
[string] $projectPath,
[string] $installTestAppsJson = ''
)

$testAppOutputFolder = Join-Path $projectPath ".buildartifacts\TestApps"

$testApps = @()
if (Test-Path $testAppOutputFolder) {
$testApps += @(Get-ChildItem -Path $testAppOutputFolder -Filter "*.app" -ErrorAction SilentlyContinue | ForEach-Object { $_.FullName })
}

if ($settings.runTestsInAllInstalledTestApps -and $installTestAppsJson -and (Test-Path $installTestAppsJson)) {
try {
$installedTestApps = @(Get-Content -Path $installTestAppsJson -Raw | ConvertFrom-Json)
}
catch {
throw "Failed to parse JSON file at path '$installTestAppsJson'. Error: $($_.Exception.Message)"
}
$testApps += @($installedTestApps | ForEach-Object { $_.TrimStart("(").TrimEnd(")") } | Where-Object { $_ -and (Test-Path $_) })
}

return @($testApps | Select-Object -Unique)
}

function Invoke-AlGoTestRun {
<#
.SYNOPSIS
Runs the normal tests for an AL-Go project against a kept-alive build container.
.DESCRIPTION
Runs tests in each test app against the given container and writes the results to
testResultsFile in JUnit format. Honors the doNotRunTests, doNotPublishApps and
treatTestFailuresAsWarnings settings. When a RunTestsInBcContainer override script is
provided, it is used instead of the built-in BcContainerHelper test runner.
.PARAMETER settings
The (analyzed) AL-Go settings hashtable.
.PARAMETER projectPath
The full path to the project folder.
.PARAMETER containerName
The name of the build container to run the tests against.
.PARAMETER credential
The credential used to connect to the build container.
.PARAMETER installTestAppsJson
Path to a JSON file with the list of installed test apps.
.PARAMETER runTestsOverride
Optional scriptblock overriding the BcContainerHelper test runner (RunTestsInBcContainer).
#>
Param(
[hashtable] $settings,
[string] $projectPath,
[string] $containerName,
[System.Management.Automation.PSCredential] $credential,
[string] $installTestAppsJson = '',
[scriptblock] $runTestsOverride = $null
)

if ($settings.doNotRunTests) {
Write-Host "doNotRunTests is set. Skipping test execution."
return
}

if ($settings.doNotPublishApps) {
Write-Host "doNotPublishApps is set, so RunPipeline did not keep a build container alive. Skipping test execution."
return
}

$testApps = Get-TestAppsToRun -settings $settings -projectPath $projectPath -installTestAppsJson $installTestAppsJson
if ($testApps.Count -eq 0) {
Write-Host "No test apps found to run tests in. Skipping test execution."
return
}

Write-Host "Running tests against container '$containerName'"

$testResultsFile = Join-Path $projectPath "TestResults.xml"
if (Test-Path $testResultsFile) {
Remove-Item $testResultsFile -Force
}

# Test failures surface as warnings when treatTestFailuresAsWarnings is set, otherwise as errors.
$gitHubActionsSeverity = if ($settings.treatTestFailuresAsWarnings) { 'warning' } else { 'error' }

$allTestsPassed = $true
Push-Location $projectPath
try {
foreach ($testApp in $testApps) {
$appJson = Get-AppJsonFromAppFile -appFile $testApp
Write-Host "Running tests in $($appJson.name) ($($appJson.id))"

$runTestsParams = @{
"containerName" = $containerName
"credential" = $credential
"companyName" = $settings.companyName
"extensionId" = $appJson.id
"appName" = $appJson.name
"JUnitResultFileName" = $testResultsFile
"AppendToJUnitResultFile" = $true
"detailed" = $true
"GitHubActions" = $gitHubActionsSeverity
"returnTrueIfAllPassed" = $true
}

if ($runTestsOverride) {
$passed = & $runTestsOverride -parameters $runTestsParams
}
else {
$passed = Run-TestsInBcContainer @runTestsParams
}

if (-not $passed) {
$allTestsPassed = $false
}
}
}
finally {
Pop-Location
}

if (-not $allTestsPassed) {
if ($settings.treatTestFailuresAsWarnings) {
OutputWarning -message "There are test failures, but they are treated as warnings (treatTestFailuresAsWarnings is set)."
}
else {
throw "There are test failures."
}
}
}

Export-ModuleMember -Function Invoke-AlGoTestRun, Get-TestAppsToRun
Loading
Loading