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
2 changes: 1 addition & 1 deletion .github/AL-Go-Settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"$schema": "https://raw.githubusercontent.com/microsoft/AL-Go/91b96c2b294be6f823277dafe6f03350abfb9d23/Actions/.Modules/settings.schema.json",
"type": "PTE",
"templateUrl": "https://github.com/microsoft/AL-Go-PTE@preview",
"bcContainerHelperVersion": "preview",
"bcContainerHelperVersion": "6.1.19-preview2811389",
"runs-on": "windows-latest",
"cacheImageName": "",
"UsePsSession": true,
Expand Down
496 changes: 76 additions & 420 deletions .github/workflows/CICD.yaml

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions .github/workflows/PullRequestHandler.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ jobs:

Initialization:
needs: [ PregateCheck ]
if: (!failure() && !cancelled())
if: (!failure() && !cancelled()) && github.head_ref != 'features/646383-sql-reset-pilot'
runs-on: [ windows-latest ]
outputs:
projects: ${{ steps.determineProjectsToBuild.outputs.ProjectsJson }}
Expand Down Expand Up @@ -93,7 +93,7 @@ jobs:

Build1:
needs: [ Initialization, CustomJob-VerifyAppChanges ]
if: (!failure()) && (!cancelled()) && fromJson(needs.Initialization.outputs.buildOrderJson)[0].projectsCount > 0
if: (!failure()) && (!cancelled()) && needs.Initialization.result == 'success' && fromJson(needs.Initialization.outputs.buildOrderJson)[0].projectsCount > 0
strategy:
matrix:
include: ${{ fromJson(needs.Initialization.outputs.buildOrderJson)[0].buildDimensions }}
Expand All @@ -119,7 +119,7 @@ jobs:

Build:
needs: [ Initialization, Build1, CustomJob-VerifyAppChanges ]
if: (!failure()) && (!cancelled()) && (needs.Build1.result == 'success' || needs.Build1.result == 'skipped') && fromJson(needs.Initialization.outputs.buildOrderJson)[1].projectsCount > 0
if: (!failure()) && (!cancelled()) && needs.Initialization.result == 'success' && (needs.Build1.result == 'success' || needs.Build1.result == 'skipped') && fromJson(needs.Initialization.outputs.buildOrderJson)[1].projectsCount > 0
strategy:
matrix:
include: ${{ fromJson(needs.Initialization.outputs.buildOrderJson)[1].buildDimensions }}
Expand Down
21 changes: 21 additions & 0 deletions build/scripts/NewBcContainer.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,28 @@ if ($platformVersion) {
$parameters.platformArtifactUrl = "$platformUrl/platform"
}

if ($env:BC_SQL_PILOT_ARM) {
& (Join-Path $PSScriptRoot 'SqlResetPilot\ContainerPreflight.ps1') -Parameters $parameters
}
New-BcContainer @parameters
if ($env:BC_SQL_PILOT_ARM) {
$runtime = Invoke-ScriptInBcContainer -containerName $parameters.ContainerName -scriptblock {
$commands = @('Get-NAVTenant', 'Dismount-NAVTenant', 'Mount-NAVTenant', 'Test-NAVDatabase', 'Copy-NAVDatabase', 'Remove-NAVDatabase')
@{
commands = @($commands | ForEach-Object {
$command = Get-Command $_ -ErrorAction Stop
@{ name = $command.Name; version = [string]$command.Version; parameters = @($command.Parameters.Keys) }
})
nst = @(Get-Process 'Microsoft.Dynamics.Nav.Server' | ForEach-Object {
@{ pid = $_.Id; version = $_.FileVersion }
})
}
}
$runtime | ConvertTo-Json -Depth 6 | Set-Content (Join-Path $env:BC_SQL_PILOT_OUTPUT 'runtime-preflight.json')
if (@($runtime.nst).Count -ne 1 -or $runtime.nst[0].version -notmatch '^30\.0\.55665\.0(?:\s|$)') {
throw 'Installed NST is not the predeclared 30.0.55665.0 runtime.'
}
}

if ($parameters.auth -in @('UserPassword', 'NavUserPassword')) {
if (-not $parameters.credential) {
Expand Down
49 changes: 43 additions & 6 deletions build/scripts/ParallelTestExecution.psm1
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ if (-not (Get-Command Write-Log -ErrorAction SilentlyContinue)) {
}
}
Import-Module (Join-Path $PSScriptRoot "ALAppBuild.psm1" -Resolve)
if ($env:BC_SQL_PILOT_ARM) {
Import-Module (Join-Path $PSScriptRoot 'SqlResetPilot\Lifecycle.psm1')
}

<#
.SYNOPSIS
Expand Down Expand Up @@ -376,9 +379,12 @@ function New-BcTestTenantTemplate {
[string]$SourceDatabaseName
)

$result = @(Invoke-ScriptInBcContainer -containerName $ContainerName -useSession $false -scriptblock { Param($sourceDatabaseName)
$result = @(Invoke-ScriptInBcContainer -containerName $ContainerName -useSession $false -scriptblock { Param($sourceDatabaseName, $sqlPilot)
$templateDatabaseName = "$sourceDatabaseName-test-template"
$maxAttempts = 3
if ($sqlPilot -and (Test-NAVDatabase -DatabaseName $templateDatabaseName)) {
throw 'Pilot template already exists; it must never be overwritten.'
}
$maxAttempts = if ($sqlPilot) { 1 } else { 3 }
$retryDelaySeconds = 5
for ($attempt = 1; $attempt -le $maxAttempts; $attempt++) {
try {
Expand All @@ -398,7 +404,7 @@ function New-BcTestTenantTemplate {
}
}
$templateDatabaseName
} -argumentList $SourceDatabaseName)
} -argumentList $SourceDatabaseName, ([bool]$env:BC_SQL_PILOT_ARM))

if ($result.Count -eq 0) {
throw "Creating the clean test tenant template returned no database name."
Expand Down Expand Up @@ -430,6 +436,12 @@ function Reset-BcTestTenant {
[string]$TemplateDatabaseName
)

if ($env:BC_SQL_PILOT_ARM) {
Invoke-SqlPilotReset -ContainerName $ContainerName -Tenant $Tenant -DatabaseName $TenantDatabaseName `
-TemplateDatabaseName $TemplateDatabaseName
return
}

Invoke-ScriptInBcContainer -containerName $ContainerName -useSession $false -scriptblock {
Param($tenant, $tenantDatabaseName, $templateDatabaseName)

Expand Down Expand Up @@ -635,13 +647,16 @@ function Receive-TestJobResult {

$outcome = 'Passed'
if ($Job.State -eq 'Failed' -or $Job.State -eq 'Stopped') {
if ((Test-TransientTestFailure $output) -and -not $Retried.ContainsKey($Entry.appName)) {
if (-not $env:BC_SQL_PILOT_ARM -and (Test-TransientTestFailure $output) -and -not $Retried.ContainsKey($Entry.appName)) {
$outcome = 'Transient'
} else {
$outcome = 'Failed'
}
}

if ($env:BC_SQL_PILOT_ARM) {
$output | Set-Content (Join-Path $env:BC_SQL_PILOT_OUTPUT "worker-$($Job.Id).log") -Encoding UTF8
}
Remove-Job -Job $Job -Force

return [PSCustomObject]@{
Expand Down Expand Up @@ -836,6 +851,9 @@ function Invoke-RequiredDisabledTestExecution {
-ScriptPath $ScriptPath -TestType $TestType -State $state -Verb $dispatch.Verb
}
$null = Wait-ForAllTestJobs -state $state
if ($env:BC_SQL_PILOT_ARM -and $state.hasFailures) {
break
}
}

return (-not $state.hasFailures)
Expand Down Expand Up @@ -1327,6 +1345,10 @@ function Invoke-ParallelTestExecution {
}

$tenantInfo = @(Get-AvailableBcTenantInfo -containerName $parameters.containerName)
if ($env:BC_SQL_PILOT_ARM) {
Initialize-SqlResetPilot -ContainerName $parameters.containerName -TenantInfo $tenantInfo `
-Arm $env:BC_SQL_PILOT_ARM -RunId $env:GITHUB_RUN_ID -OutputDirectory $env:BC_SQL_PILOT_OUTPUT
}
$tenants = @($tenantInfo | ForEach-Object { $_.Id })
Write-Host "Available tenants: $($tenants -join ', ')"

Expand Down Expand Up @@ -1384,6 +1406,11 @@ function Invoke-ParallelTestExecution {
-AppNamesToTest $appNamesToTest -AppIdByName $appIdByName
)
}
if ($env:BC_SQL_PILOT_ARM) {
$requiredDisabledWorkItems = @(Select-SqlPilotPrefix -WorkItems $requiredDisabledWorkItems)
$requiredDisabledWorkItems | ConvertTo-Json -Depth 5 |
Set-Content (Join-Path $env:BC_SQL_PILOT_OUTPUT 'ordered-prefix.json') -Encoding UTF8
}
$templateDatabaseName = ""
try {
if ($requiredDisabledWorkItems.Count -gt 0) {
Expand Down Expand Up @@ -1417,13 +1444,23 @@ function Invoke-ParallelTestExecution {
}
}
finally {
foreach ($cleanTenant in $cleanTenantInfo) {
foreach ($cleanTenant in @($cleanTenantInfo | Where-Object { -not $env:BC_SQL_PILOT_ARM })) {
Reset-BcTestTenant -ContainerName $parameters.containerName -Tenant $cleanTenant.Id `
-TenantDatabaseName $cleanTenant.DatabaseName -TemplateDatabaseName $templateDatabaseName
}
}
}

if ($env:BC_SQL_PILOT_ARM) {
Merge-TenantTestResults -parameters $parameters -tenants $tenants -workItems $requiredDisabledWorkItems -rerunSuffixes @()
$state.finalResult = -not $state.hasFailures
$state.completed = $true
$state | ConvertTo-Json -Depth 5 | Set-Content $stateFile -Force
@{ completedUtc = [DateTime]::UtcNow.ToString('o'); passed = $state.finalResult; selectedCodeunits = $requiredDisabledWorkItems.Count } |
ConvertTo-Json | Set-Content (Join-Path $env:BC_SQL_PILOT_OUTPUT 'outcome.json') -Encoding UTF8
return $state.finalResult
}

# Single dispatch loop, FIFO. TestConfiguration.json lists the smallest app first (a cheap
# serial warmup) and the rest longest-first (LPT, keeps the tail short). The retry cap lives in
# Receive-TestJobResult: an app already in $state.retried is classified as Failed on a re-fail.
Expand Down Expand Up @@ -1521,7 +1558,7 @@ function Invoke-ParallelTestExecution {
return $allPassed
}
finally {
if ($templateDatabaseName) {
if ($templateDatabaseName -and -not $env:BC_SQL_PILOT_ARM) {
Remove-BcTestTenantTemplate -ContainerName $parameters.containerName `
-TemplateDatabaseName $templateDatabaseName
}
Expand Down
34 changes: 34 additions & 0 deletions build/scripts/SqlResetPilot/ContainerPreflight.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
param([hashtable]$Parameters)
$ErrorActionPreference = 'Stop'
if ($env:GITHUB_REF -ne 'refs/heads/features/646383-sql-reset-pilot' -or
$env:GITHUB_EVENT_NAME -ne 'workflow_dispatch' -or $env:GITHUB_RUN_ATTEMPT -ne '1' -or
$env:BC_SQL_PILOT_ARM -notin @('control', 'fresh') -or -not $env:BC_SQL_PILOT_OUTPUT) {
throw 'Refusing SQL pilot outside its disposable manually dispatched CI job.'
}
$expectedName = "bcbuildprojectsTestAppsW1$($env:GITHUB_RUN_ID)"
if ($Parameters.ContainerName -ne $expectedName -or (Test-BcContainer -containerName $expectedName)) {
throw 'Pilot container name is unexpected or already exists. No replacement is authorized.'
}
$module = Get-Module BcContainerHelper -ErrorAction Stop
$helperVersion = & $module { $bcContainerHelperVersion }
if ($helperVersion -ne '6.1.19-preview2811389') { throw "Wrong BCH version: $helperVersion" }
$settings = $env:Settings | ConvertFrom-Json
if ($settings.country -ne 'w1' -or $settings.testType -ne 'IntegrationTest' -or
$settings.numberOfTenantsForTesting -ne 4 -or $settings.companyName -ne 'My Company' -or
-not $settings.enableCleanTestCodeunitExecution -or $settings.enableTaskScheduler) {
throw 'Effective W1 Integration settings differ from the predeclared experiment.'
}
if (-not (Get-Command New-BcContainer).Parameters.ContainsKey('useGenericImage')) {
throw 'Exact BCH does not expose the supported generic-image parameter.'
}
$Parameters.useGenericImage = 'mcr.microsoft.com/businesscentral@sha256:c899d12093ad7bbdbfd08ccc0e6294e0f98c682c7e35db4ecfbca345fb068492'
if ($Parameters.platformArtifactUrl -ne 'https://bcinsider-fvh2ekdjecfjd6gk.b02.azurefd.net/platform/30.0.55665.0/platform') {
throw 'Platform artifact is not the pinned source platform.'
}
@{
container = $expectedName; runId = $env:GITHUB_RUN_ID; arm = $env:BC_SQL_PILOT_ARM
helperVersion = $helperVersion; image = $Parameters.useGenericImage
imageGenericTag = '1.0.2.128'; platform = '30.0.55665.0'
hostOS = [Environment]::OSVersion.Version.ToString()
registeredUtc = [DateTime]::UtcNow.ToString('o')
} | ConvertTo-Json | Set-Content (Join-Path $env:BC_SQL_PILOT_OUTPUT 'container-ownership.json') -Encoding UTF8
53 changes: 53 additions & 0 deletions build/scripts/SqlResetPilot/Finalize.Tests.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
function global:Test-BcContainer { param($containerName) throw 'Live container call forbidden.' }
function global:Get-BcContainerEventLog { param($containerName, [switch]$doNotOpen) throw 'Live container call forbidden.' }
function global:Remove-BcContainer { param($containerName) throw 'Live container call forbidden.' }

Describe 'SQL pilot final cleanup ownership' {
BeforeEach {
$script:oldWorkspace = $env:GITHUB_WORKSPACE
$script:oldRun = $env:GITHUB_RUN_ID
$script:oldArm = $env:BC_SQL_PILOT_ARM
$script:oldHelper = $env:BcContainerHelperPath
$env:GITHUB_WORKSPACE = $PSScriptRoot
$env:GITHUB_RUN_ID = '123'
$env:BC_SQL_PILOT_ARM = 'fresh'
$env:BcContainerHelperPath = Join-Path $PSScriptRoot 'BcContainerHelper.ps1'
Mock Import-Module {}
Mock Test-Path { $true }
Mock Get-Content { '{"runId":"123","arm":"fresh","container":"bcbuildprojectsTestAppsW1123"}' }
Mock Get-ChildItem {}
Mock Copy-Item {}
Mock Set-Content {}
$global:sqlPilotTestContainerExists = $true
Mock Test-BcContainer { $global:sqlPilotTestContainerExists }
Mock Get-BcContainerEventLog { 'events.evtx' }
Mock Remove-BcContainer { $global:sqlPilotTestContainerExists = $false }
}
AfterEach {
$env:GITHUB_WORKSPACE = $script:oldWorkspace
$env:GITHUB_RUN_ID = $script:oldRun
$env:BC_SQL_PILOT_ARM = $script:oldArm
$env:BcContainerHelperPath = $script:oldHelper
Remove-Variable sqlPilotTestContainerExists -Scope Global
}
It 'exports before disposing only the registered container' {
& (Join-Path $PSScriptRoot 'Finalize.ps1')
Assert-MockCalled Get-BcContainerEventLog -Times 1 -Exactly -Scope It
Assert-MockCalled Remove-BcContainer -Times 1 -Exactly -Scope It -ParameterFilter { $containerName -eq 'bcbuildprojectsTestAppsW1123' }
}
It 'still tears down if exporting fails and keeps that failure visible' {
Mock Get-BcContainerEventLog { throw 'event export failed' }
{ & (Join-Path $PSScriptRoot 'Finalize.ps1') } | Should -Throw
Assert-MockCalled Remove-BcContainer -Times 1 -Exactly -Scope It
}
It 'refuses deletion if ownership was not recorded' {
Mock Test-Path { $false }
& (Join-Path $PSScriptRoot 'Finalize.ps1')
Assert-MockCalled Remove-BcContainer -Times 0 -Exactly -Scope It
}
It 'refuses deletion when run, arm or exact name differs' {
Mock Get-Content { '{"runId":"999","arm":"fresh","container":"shared"}' }
{ & (Join-Path $PSScriptRoot 'Finalize.ps1') } | Should -Throw
Assert-MockCalled Remove-BcContainer -Times 0 -Exactly -Scope It
}
}
28 changes: 28 additions & 0 deletions build/scripts/SqlResetPilot/Finalize.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
$ErrorActionPreference = 'Stop'
$output = Join-Path $env:GITHUB_WORKSPACE 'sql-reset-pilot-output'
$ownershipPath = Join-Path $output 'container-ownership.json'
if (-not (Test-Path $ownershipPath)) {
Write-Host 'No owned container was registered; no container deletion is authorized.'
return
}
$ownership = Get-Content $ownershipPath -Raw | ConvertFrom-Json
if ($ownership.runId -ne $env:GITHUB_RUN_ID -or $ownership.arm -ne $env:BC_SQL_PILOT_ARM -or
$ownership.container -ne "bcbuildprojectsTestAppsW1$($env:GITHUB_RUN_ID)") {
throw 'Cleanup ownership mismatch.'
}
Import-Module (Join-Path (Split-Path $env:BcContainerHelperPath -Parent) 'BcContainerHelper.psm1') -ErrorAction Stop
try {
if (Test-BcContainer -containerName $ownership.container) {
$events = Get-BcContainerEventLog -containerName $ownership.container -doNotOpen
Copy-Item $events (Join-Path $output 'final-container.evtx')
}
$project = Join-Path $env:GITHUB_WORKSPACE 'build\projects\Test Apps W1'
Get-ChildItem $project -Filter 'TestResults*.xml' -File | Copy-Item -Destination $output
} finally {
if (Test-BcContainer -containerName $ownership.container) {
Remove-BcContainer -containerName $ownership.container
}
@{ completedUtc = [DateTime]::UtcNow.ToString('o'); containerRemaining = (Test-BcContainer -containerName $ownership.container) } |
ConvertTo-Json | Set-Content (Join-Path $output 'cleanup.json') -Encoding UTF8
}
if (Test-BcContainer -containerName $ownership.container) { throw 'Owned container teardown did not complete.' }
Loading
Loading