Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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"
},
"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 {
<#
.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
92 changes: 92 additions & 0 deletions Actions/RunTests/RunTests.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
Param(
[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 against the build container that the
RunPipeline action created and kept alive. This runs as part of the build when the
useSeparateTestAction setting is enabled; when it is not, RunPipeline runs the tests instead.
Results are written 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 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

# Surface the token so RunTestsInBcContainer override scripts (e.g. BCApps test tolerance) can authenticate against GitHub.
$ENV:_token = $token

# 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']
145 changes: 145 additions & 0 deletions Actions/RunTests/RunTests.psm1
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
<#
.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 treatTestFailuresAsWarnings setting. 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
)

$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
35 changes: 35 additions & 0 deletions Actions/RunTests/action.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
name: Run Tests
author: Microsoft Corporation
inputs:
shell:
description: Shell in which you want to run the action (powershell or pwsh)
required: false
default: powershell
token:
description: The GitHub token running the action
required: false
default: ${{ github.token }}
project:
description: Project folder
required: false
default: '.'
installTestAppsJson:
description: A path to a JSON-formatted list of test apps to run tests in
required: false
default: ''
runs:
using: composite
steps:
- name: run
shell: ${{ inputs.shell }}
env:
_token: ${{ inputs.token }}
_project: ${{ inputs.project }}
_installTestAppsJson: ${{ inputs.installTestAppsJson }}
run: |
${{ github.action_path }}/../Invoke-AlGoAction.ps1 -ActionName "RunTests" -Action {
${{ github.action_path }}/RunTests.ps1 -token $ENV:_token -project $ENV:_project -installTestAppsJson $ENV:_installTestAppsJson
}
branding:
icon: terminal
color: blue
4 changes: 4 additions & 0 deletions RELEASENOTES.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
### Separate test execution from RunPipeline (PREVIEW)

A new `useSeparateTestAction` setting (default `false`) lets you move normal test execution (`testFolders`) out of the `RunPipeline` action and into a new dedicated `RunTests` action. When enabled, `RunPipeline` still compiles, publishes and installs the apps and keeps the build container alive, but does not run the normal tests. A new `RunTests` action then runs the tests against that same container and produces the same `TestResults.xml`. Only normal tests are affected; BCPT and page scripting tests are still executed by `RunPipeline`. When the setting is `false`, behavior is unchanged.

### New `doNotPerformUpgrade` setting

AL-Go now supports a new `doNotPerformUpgrade` setting that is passed through to `Run-AlPipeline`. Use it to skip the upgrade phase while still running the rest of the pipeline.
Expand Down
Loading
Loading