From 2fc91ea87a5adc622fa4db4ec26691092fd56cc0 Mon Sep 17 00:00:00 2001 From: Daniel Marbach Date: Sun, 9 Aug 2026 00:19:42 +0200 Subject: [PATCH 1/3] Spike: run test categories as background lanes vs serial on a single Windows runner --- .github/workflows/spike-lanes.yml | 124 ++++++++++++++++++++++++++++++ tools/spike-memory.ps1 | 38 +++++++++ tools/spike-run-lane.ps1 | 36 +++++++++ 3 files changed, 198 insertions(+) create mode 100644 .github/workflows/spike-lanes.yml create mode 100644 tools/spike-memory.ps1 create mode 100644 tools/spike-run-lane.ps1 diff --git a/.github/workflows/spike-lanes.yml b/.github/workflows/spike-lanes.yml new file mode 100644 index 0000000000..d8b7793899 --- /dev/null +++ b/.github/workflows/spike-lanes.yml @@ -0,0 +1,124 @@ +# Spike: can we run more categories per OS runner using the new background/wait +# primitives? Windows-only, no cloud categories, no secrets required. +# +# Two jobs, same build, same five category groups (lanes), different scheduling: +# +# spike-serial : runs the lanes one after another on a single runner +# spike-lanes : runs the lanes as `background: true` steps, joined with wait-all +# +# What the run answers: +# 1. Machine minutes: serial >= lanes should collapse if the work is I/O bound. +# 2. 16 GB ceiling: do five acceptance lanes (ServiceControl hosts + RavenDB +# embedded servers each) fit, or do lanes OOM/swap? +# 3. Ports: lane-multi hosts primary+audit (33333/44444) while lane-raven-main +# and lane-audit host those same instance types concurrently. Bind failures +# tell us the acceptance harness uses fixed ports. +# +# Only for the ci_tryout branch. Intentionally not wired into the main matrix. + +name: Spike Lanes + +on: + workflow_dispatch: + push: + branches: + - ci_tryout + +concurrency: + group: spike-lanes-${{ github.ref }} + cancel-in-progress: true + +env: + PARTICULARSOFTWARE_LICENSE: ${{ secrets.LICENSETEXT }} + +defaults: + run: + shell: pwsh + +jobs: + spike-serial: + name: Spike Serial (baseline) + runs-on: windows-latest + timeout-minutes: 90 + steps: + - name: Checkout + uses: actions/checkout@v7.0.1 + - name: Setup .NET SDK + uses: actions/setup-dotnet@v6.0.0 + with: + global-json-file: global.json + - name: Download RavenDB Server + run: ./tools/download-ravendb-server.ps1 + - name: Build + run: dotnet build src --configuration Release -graph + - name: Run lanes sequentially + run: | + ./tools/spike-run-lane.ps1 -Name raven-main -Filter PrimaryRavenAcceptance -Projects @( + 'src/ServiceControl.AcceptanceTests.RavenDB/ServiceControl.AcceptanceTests.RavenDB.csproj') + ./tools/spike-run-lane.ps1 -Name audit -Filter Default -Projects @( + 'src/ServiceControl.Audit.AcceptanceTests/ServiceControl.Audit.AcceptanceTests.csproj', + 'src/ServiceControl.Audit.AcceptanceTests.RavenDB/ServiceControl.Audit.AcceptanceTests.RavenDB.csproj') + ./tools/spike-run-lane.ps1 -Name multi -Filter Default -Projects @( + 'src/ServiceControl.MultiInstance.AcceptanceTests/ServiceControl.MultiInstance.AcceptanceTests.csproj') + ./tools/spike-run-lane.ps1 -Name monitor -Filter Default -Projects @( + 'src/ServiceControl.Monitoring.AcceptanceTests/ServiceControl.Monitoring.AcceptanceTests.csproj', + 'src/ServiceControl.Monitoring.UnitTests/ServiceControl.Monitoring.UnitTests.csproj') + ./tools/spike-run-lane.ps1 -Name unit -Filter Default -Projects @( + 'src/ServiceControl.UnitTests/ServiceControl.UnitTests.csproj', + 'src/ServiceControl.Audit.UnitTests/ServiceControl.Audit.UnitTests.csproj', + 'src/ServiceControl.Config.Tests/ServiceControl.Config.Tests.csproj', + 'src/ServiceControl.Infrastructure.Tests/ServiceControl.Infrastructure.Tests.csproj', + 'src/Particular.LicensingComponent.UnitTests/Particular.LicensingComponent.UnitTests.csproj', + 'src/ServiceControl.Persistence.Tests.InMemory/ServiceControl.Persistence.Tests.InMemory.csproj', + 'src/ServiceControl.Audit.Persistence.Tests/ServiceControl.Audit.Persistence.Tests.csproj', + 'src/ServiceControl.Audit.Persistence.Tests.RavenDB/ServiceControl.Audit.Persistence.Tests.RavenDB.csproj', + 'src/ServiceControlInstaller.Engine.UnitTests/ServiceControlInstaller.Engine.UnitTests.csproj', + 'src/ServiceControlInstaller.Packaging.UnitTests/ServiceControlInstaller.Packaging.UnitTests.csproj', + 'src/ServiceControl.Transports.Tests/ServiceControl.Transports.Tests.csproj') + + spike-lanes: + name: Spike Lanes (parallel) + runs-on: windows-latest + timeout-minutes: 90 + steps: + - name: Checkout + uses: actions/checkout@v7.0.1 + - name: Setup .NET SDK + uses: actions/setup-dotnet@v6.0.0 + with: + global-json-file: global.json + - name: Download RavenDB Server + run: ./tools/download-ravendb-server.ps1 + - name: Build + run: dotnet build src --configuration Release -graph + - name: Memory sampler + id: memory + run: ./tools/spike-memory.ps1 + background: true + - name: Lane raven-main (hosts main SC, 33333) + id: lane-raven-main + run: ./tools/spike-run-lane.ps1 -Name raven-main -Filter PrimaryRavenAcceptance -Projects @('src/ServiceControl.AcceptanceTests.RavenDB/ServiceControl.AcceptanceTests.RavenDB.csproj') + background: true + - name: Lane audit (hosts audit SC, 44444) + id: lane-audit + run: ./tools/spike-run-lane.ps1 -Name audit -Filter Default -Projects @('src/ServiceControl.Audit.AcceptanceTests/ServiceControl.Audit.AcceptanceTests.csproj', 'src/ServiceControl.Audit.AcceptanceTests.RavenDB/ServiceControl.Audit.AcceptanceTests.RavenDB.csproj') + background: true + - name: Lane multi (hosts primary AND audit) + id: lane-multi + run: ./tools/spike-run-lane.ps1 -Name multi -Filter Default -Projects @('src/ServiceControl.MultiInstance.AcceptanceTests/ServiceControl.MultiInstance.AcceptanceTests.csproj') + background: true + - name: Lane monitor (hosts monitoring SC, 33633) + id: lane-monitor + run: ./tools/spike-run-lane.ps1 -Name monitor -Filter Default -Projects @('src/ServiceControl.Monitoring.AcceptanceTests/ServiceControl.Monitoring.AcceptanceTests.csproj', 'src/ServiceControl.Monitoring.UnitTests/ServiceControl.Monitoring.UnitTests.csproj') + background: true + - name: Lane unit (no fixed ports) + id: lane-unit + run: ./tools/spike-run-lane.ps1 -Name unit -Filter Default -Projects @('src/ServiceControl.UnitTests/ServiceControl.UnitTests.csproj', 'src/ServiceControl.Audit.UnitTests/ServiceControl.Audit.UnitTests.csproj', 'src/ServiceControl.Config.Tests/ServiceControl.Config.Tests.csproj', 'src/ServiceControl.Infrastructure.Tests/ServiceControl.Infrastructure.Tests.csproj', 'src/Particular.LicensingComponent.UnitTests/Particular.LicensingComponent.UnitTests.csproj', 'src/ServiceControl.Persistence.Tests.InMemory/ServiceControl.Persistence.Tests.InMemory.csproj', 'src/ServiceControl.Audit.Persistence.Tests/ServiceControl.Audit.Persistence.Tests.csproj', 'src/ServiceControl.Audit.Persistence.Tests.RavenDB/ServiceControl.Audit.Persistence.Tests.RavenDB.csproj', 'src/ServiceControlInstaller.Engine.UnitTests/ServiceControlInstaller.Engine.UnitTests.csproj', 'src/ServiceControlInstaller.Packaging.UnitTests/ServiceControlInstaller.Packaging.UnitTests.csproj', 'src/ServiceControl.Transports.Tests/ServiceControl.Transports.Tests.csproj') + background: true + - name: Wait for lanes + wait-all: + - name: Memory summary + if: always() + run: | + Get-Content -Path .\spike-memory.csv -ErrorAction SilentlyContinue | Select-Object -First 3 + Get-Content -Path .\spike-memory.csv -ErrorAction SilentlyContinue | Select-Object -Last 10 \ No newline at end of file diff --git a/tools/spike-memory.ps1 b/tools/spike-memory.ps1 new file mode 100644 index 0000000000..9c504bf6c8 --- /dev/null +++ b/tools/spike-memory.ps1 @@ -0,0 +1,38 @@ +# Spike support script for the "background lanes" CI experiment. +# Samples available physical memory every 5 seconds and writes a CSV, so we can +# see whether concurrent acceptance-test lanes fit in the 16 GB runner. Stops +# early once every lane (5 sentinel-* files) has finished, or after MaxSeconds. + +param( + [int]$MaxSeconds = 1800 +) + +$log = 'spike-memory.csv' +$start = Get-Date +$minAvailableGb = [double]::MaxValue +$samples = 0 + +while (((Get-Date) - $start).TotalSeconds -lt $MaxSeconds) { + try { + $availableGb = (Get-CimInstance Win32_OperatingSystem).FreePhysicalMemory / 1MB + } + catch { + $availableGb = -1 + } + + "$(Get-Date -Format HH:mm:ss),$([math]::Round($availableGb, 1))" | Add-Content -Path $log + if ($availableGb -ge 0 -and $availableGb -lt $minAvailableGb) { + $minAvailableGb = $availableGb + } + $samples++ + + $doneLanes = @(Get-ChildItem -Path . -Filter 'sentinel-*' -ErrorAction SilentlyContinue) + if ($doneLanes.Count -ge 5) { + break + } + + Start-Sleep -Seconds 5 +} + +Write-Output "Memory samples: $samples, min available: $([math]::Round($minAvailableGb, 1)) GB" +Get-Content -Path $log | Select-Object -Last 5 \ No newline at end of file diff --git a/tools/spike-run-lane.ps1 b/tools/spike-run-lane.ps1 new file mode 100644 index 0000000000..87a0bc37b3 --- /dev/null +++ b/tools/spike-run-lane.ps1 @@ -0,0 +1,36 @@ +# Spike support script for the "background lanes" CI experiment. +# Runs one test category lane: an explicit list of test projects with the +# ServiceControl_TESTS_FILTER env var set, mirroring what run-tests-action does +# on master. Touches a sentinel file when done so the memory sampler can stop. +# Exit code reflects any failing project, but the sentinel is written either way. + +param( + [Parameter(Mandatory)] + [string]$Name, + + [Parameter(Mandatory)] + [string[]]$Projects, + + [string]$Filter = 'Default' +) + +$ErrorActionPreference = 'Continue' + +$env:ServiceControl_TESTS_FILTER = $Filter +Write-Output "Lane '$Name' filter='$Filter' projects=$($Projects.Count)" + +$exitCode = 0 + +foreach ($project in $Projects) { + Write-Output "::group::$Name :: $project" + dotnet test $project --configuration Release --no-build --logger "GitHubActions" + if ($LASTEXITCODE -ne 0) { + Write-Output "::error::$Name :: $project exited with $LASTEXITCODE" + $exitCode = 1 + } + Write-Output "::endgroup::" +} + +New-Item -ItemType File -Path "sentinel-$Name" -Force | Out-Null + +exit $exitCode \ No newline at end of file From 59b1fe5596857df2d5bfcfe2f5e8232f3787844e Mon Sep 17 00:00:00 2001 From: Daniel Marbach Date: Sun, 9 Aug 2026 00:42:33 +0200 Subject: [PATCH 2/3] CI: build once per OS, publish test outputs to Azure blob, download per category --- .github/workflows/ci.yml | 263 ++++++++++++++++++++++++++---- .github/workflows/spike-lanes.yml | 124 -------------- tools/ci-category-projects.ps1 | 62 +++++++ tools/run-tests.ps1 | 54 ++++++ tools/spike-memory.ps1 | 38 ----- tools/spike-run-lane.ps1 | 36 ---- 6 files changed, 348 insertions(+), 229 deletions(-) delete mode 100644 .github/workflows/spike-lanes.yml create mode 100644 tools/ci-category-projects.ps1 create mode 100644 tools/run-tests.ps1 delete mode 100644 tools/spike-memory.ps1 delete mode 100644 tools/spike-run-lane.ps1 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d3bcdfb490..a62fc8e767 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,15 +6,207 @@ on: - release-* pull_request: workflow_dispatch: +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true env: DOTNET_NOLOGO: true defaults: run: shell: pwsh jobs: - build: + # Build each OS once and publish the test outputs to Azure blob storage. Every + # test job downloads its category's closure instead of rebuilding the solution + # 26 times. Because of the `needs:` gates below, a failed build skips all test + # jobs, so no broker/cloud resource is ever provisioned for an unbuildable commit. + build-windows: + name: Windows Build + runs-on: windows-latest + timeout-minutes: 90 + env: + AZURE_ACI_CREDENTIALS: ${{ secrets.AZURE_ACI_CREDENTIALS }} + AZURE_CI_RG: ServiceControl-CI + AZURE_CI_ACCOUNT: scpartpciartifacts + outputs: + account: ${{ steps.publish.outputs.account }} + container: ${{ steps.publish.outputs.container }} + sas: ${{ steps.publish.outputs.sas }} + steps: + - name: Check for secrets + env: + SECRETS_AVAILABLE: ${{ secrets.SECRETS_AVAILABLE }} + run: exit $(If ($env:SECRETS_AVAILABLE -eq 'true') { 0 } Else { 1 }) + - name: Checkout + uses: actions/checkout@v7.0.1 + with: + fetch-depth: 0 + - name: Setup .NET SDK + uses: actions/setup-dotnet@v6.0.0 + with: + global-json-file: global.json + # Needed by ServiceControlInstaller.Packaging's zip targets during the build. + - name: Download RavenDB Server + run: ./tools/download-ravendb-server.ps1 + - name: Build + run: dotnet build src --configuration Release -graph + - name: Zip PowerShell module + run: | + New-Item assets\PowerShellModules -ItemType Directory + Compress-Archive -Path deploy\PowerShellModules\Particular.ServiceControl.Management\* -DestinationPath assets\PowerShellModules\Particular.ServiceControl.Management.zip + - name: Smoke test PowerShell module import + run: Import-Module ./deploy/PowerShellModules/Particular.ServiceControl.Management + - name: Upload assets + uses: actions/upload-artifact@v7.0.1 + with: + name: Windows-assets + path: | + nugets/ + zip/ + assets/ + retention-days: 1 + - name: Azure login + uses: azure/login@v3.0.1 + with: + creds: ${{ secrets.AZURE_ACI_CREDENTIALS }} + - name: Publish test outputs + id: publish + run: | + Write-Output "Ensuring resource group and storage account" + az group create --name $env:AZURE_CI_RG --location westeurope --only-show-errors | Out-Null + for ($attempt = 0; $attempt -lt 3; $attempt++) { + $exists = az storage account show --name $env:AZURE_CI_ACCOUNT --resource-group $env:AZURE_CI_RG --query name --output tsv 2>$null + if ($exists) { break } + az storage account create --name $env:AZURE_CI_ACCOUNT --resource-group $env:AZURE_CI_RG --sku Standard_LRS --kind StorageV2 --only-show-errors | Out-Null + if ($LASTEXITCODE -eq 0) { break } + Start-Sleep -Seconds 15 + } + $key = az storage account keys list --account-name $env:AZURE_CI_ACCOUNT --resource-group $env:AZURE_CI_RG --query "[0].value" --output tsv + if (-not $key) { throw 'Could not obtain storage account key' } + + $container = "run-$($env:GITHUB_RUN_ID)-win" + az storage container create --account-name $env:AZURE_CI_ACCOUNT --account-key $key --name $container --output none + + $expiry = (Get-Date).ToUniversalTime().AddHours(8).ToString('yyyy-MM-ddTHH:mmZ') + $sas = az storage container generate-sas --account-name $env:AZURE_CI_ACCOUNT --account-key $key --name $container --permissions rl --expiry $expiry --output tsv + + $testProjects = Get-ChildItem -Path src -Recurse -Filter 'TestsFilter.cs' -File | ForEach-Object { $_.Directory.Name } | Sort-Object -Unique + $staging = Join-Path $env:GITHUB_WORKSPACE '.staging' + $staged = 0 + foreach ($p in $testProjects) { + $source = Join-Path 'src' $p + if (-not (Test-Path (Join-Path $source 'bin\Release'))) { continue } + $dest = Join-Path $staging "src\$p" + New-Item -ItemType Directory -Force -Path $dest | Out-Null + Copy-Item -Path (Join-Path $source 'bin\Release') -Destination (Join-Path $dest 'bin') -Recurse -Force + Copy-Item -Path (Join-Path $source 'obj') -Destination (Join-Path $dest 'obj') -Recurse -Force + $staged++ + } + Write-Output "Staged $staged test projects" + + az storage blob upload-batch --account-name $env:AZURE_CI_ACCOUNT --account-key $key --destination $container --source $staging --pattern "**" --only-show-errors + if ($LASTEXITCODE -ne 0) { throw 'Blob upload failed' } + + "account=$($env:AZURE_CI_ACCOUNT)" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append + "container=$container" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append + "sas=$sas" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append + + build-linux: + name: Linux Build + runs-on: ubuntu-latest + timeout-minutes: 90 + env: + AZURE_ACI_CREDENTIALS: ${{ secrets.AZURE_ACI_CREDENTIALS }} + AZURE_CI_RG: ServiceControl-CI + AZURE_CI_ACCOUNT: scpartpciartifacts + outputs: + account: ${{ steps.publish.outputs.account }} + container: ${{ steps.publish.outputs.container }} + sas: ${{ steps.publish.outputs.sas }} + steps: + - name: Check for secrets + env: + SECRETS_AVAILABLE: ${{ secrets.SECRETS_AVAILABLE }} + run: exit $(If ($env:SECRETS_AVAILABLE -eq 'true') { 0 } Else { 1 }) + - name: Checkout + uses: actions/checkout@v7.0.1 + with: + fetch-depth: 0 + - name: Setup .NET SDK + uses: actions/setup-dotnet@v6.0.0 + with: + global-json-file: global.json + - name: Download RavenDB Server + run: ./tools/download-ravendb-server.ps1 + - name: Build + run: dotnet build src --configuration Release -graph + - name: Zip PowerShell module + run: | + New-Item assets\PowerShellModules -ItemType Directory + Compress-Archive -Path deploy\PowerShellModules\Particular.ServiceControl.Management\* -DestinationPath assets\PowerShellModules\Particular.ServiceControl.Management.zip + - name: Upload assets + uses: actions/upload-artifact@v7.0.1 + with: + name: Linux-assets + path: | + nugets/ + zip/ + assets/ + retention-days: 1 + - name: Azure login + uses: azure/login@v3.0.1 + with: + creds: ${{ secrets.AZURE_ACI_CREDENTIALS }} + - name: Publish test outputs + id: publish + run: | + Write-Output "Ensuring resource group and storage account" + az group create --name $env:AZURE_CI_RG --location westeurope --only-show-errors | Out-Null + for ($attempt = 0; $attempt -lt 3; $attempt++) { + $exists = az storage account show --name $env:AZURE_CI_ACCOUNT --resource-group $env:AZURE_CI_RG --query name --output tsv 2>$null + if ($exists) { break } + az storage account create --name $env:AZURE_CI_ACCOUNT --resource-group $env:AZURE_CI_RG --sku Standard_LRS --kind StorageV2 --only-show-errors | Out-Null + if ($LASTEXITCODE -eq 0) { break } + Start-Sleep -Seconds 15 + } + $key = az storage account keys list --account-name $env:AZURE_CI_ACCOUNT --resource-group $env:AZURE_CI_RG --query "[0].value" --output tsv + if (-not $key) { throw 'Could not obtain storage account key' } + + $container = "run-$($env:GITHUB_RUN_ID)-linux" + az storage container create --account-name $env:AZURE_CI_ACCOUNT --account-key $key --name $container --output none + + $expiry = (Get-Date).ToUniversalTime().AddHours(8).ToString('yyyy-MM-ddTHH:mmZ') + $sas = az storage container generate-sas --account-name $env:AZURE_CI_ACCOUNT --account-key $key --name $container --permissions rl --expiry $expiry --output tsv + + $testProjects = Get-ChildItem -Path src -Recurse -Filter 'TestsFilter.cs' -File | ForEach-Object { $_.Directory.Name } | Sort-Object -Unique + $staging = Join-Path $env:GITHUB_WORKSPACE '.staging' + $staged = 0 + foreach ($p in $testProjects) { + $source = Join-Path 'src' $p + if (-not (Test-Path (Join-Path $source 'bin/Release'))) { continue } + $dest = Join-Path $staging "src/$p" + New-Item -ItemType Directory -Force -Path $dest | Out-Null + Copy-Item -Path (Join-Path $source 'bin/Release') -Destination (Join-Path $dest 'bin') -Recurse -Force + Copy-Item -Path (Join-Path $source 'obj') -Destination (Join-Path $dest 'obj') -Recurse -Force + $staged++ + } + Write-Output "Staged $staged test projects" + + az storage blob upload-batch --account-name $env:AZURE_CI_ACCOUNT --account-key $key --destination $container --source $staging --pattern "**" --only-show-errors + if ($LASTEXITCODE -ne 0) { throw 'Blob upload failed' } + + "account=$($env:AZURE_CI_ACCOUNT)" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append + "container=$container" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append + "sas=$sas" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append + + test: name: ${{ matrix.os-name }}-${{ matrix.test-category }} + needs: ${{ matrix.os-name == 'Windows' && 'build-windows' || 'build-linux' }} runs-on: ${{ matrix.os }} + timeout-minutes: 90 + env: + AZURE_CI_ACCOUNT: ${{ needs[format('build-{0}', lower(matrix.os-name))].outputs.account }} + AZURE_CI_CONTAINER: ${{ needs[format('build-{0}', lower(matrix.os-name))].outputs.container }} + AZURE_CI_SAS: ${{ needs[format('build-{0}', lower(matrix.os-name))].outputs.sas }} strategy: matrix: os: [windows-latest, ubuntu-latest] @@ -48,25 +240,14 @@ jobs: uses: Particular/setup-wsl-action@v1.1.0 - name: Download RavenDB Server run: ./tools/download-ravendb-server.ps1 - - name: Build - run: dotnet build src --configuration Release -graph - - name: Zip PowerShell module + - name: Download test outputs run: | - New-Item assets\PowerShellModules -ItemType Directory - Compress-Archive -Path deploy\PowerShellModules\Particular.ServiceControl.Management\* -DestinationPath assets\PowerShellModules\Particular.ServiceControl.Management.zip - - name: Upload assets - uses: actions/upload-artifact@v7.0.1 - if: matrix.test-category == 'Default' - with: - name: ${{ matrix.os-name }}-assets - path: | - nugets/ - zip/ - assets/ - retention-days: 1 - - name: Smoke test PowerShell module import - if: matrix.os-name == 'Windows' - run: Import-Module ./deploy/PowerShellModules/Particular.ServiceControl.Management + $projects = ./tools/ci-category-projects.ps1 -Category '${{ matrix.test-category }}' + $projects | ForEach-Object { + Write-Output "Downloading $_" + az storage blob download-batch --account-name $env:AZURE_CI_ACCOUNT --sas-token $env:AZURE_CI_SAS --destination $env:GITHUB_WORKSPACE --source $env:AZURE_CI_CONTAINER --pattern "$_/**" --output none + if ($LASTEXITCODE -ne 0) { throw "Download failed for $_" } + } # there is an issue with az cli and python 3.14, so for now we need to pin it # once the issue is resolved it should be able to be re-floated @@ -84,11 +265,6 @@ jobs: python -m pip install --user "azure-cli==2.64.0" echo "$HOME/.local/bin" >> "$GITHUB_PATH" - - name: Azure login - uses: azure/login@v3.0.1 - if: matrix.test-category == 'AzureServiceBus' || matrix.test-category == 'AzureStorageQueues' || matrix.test-category == 'RabbitMQ' || matrix.test-category == 'PostgreSQL' || matrix.test-category == 'PostgreSQLPersistence' - with: - creds: ${{ secrets.AZURE_ACI_CREDENTIALS }} - name: Setup SQL Server uses: Particular/install-sql-server-action@v3.0.0 if: matrix.test-category == 'SqlServer' @@ -148,24 +324,49 @@ jobs: while ((docker inspect --format '{{.State.Health.Status}}' ibmmq) -ne 'healthy') { Start-Sleep -Seconds 2 } - echo "ServiceControl_TransportTests_IBMMQ_ConnectionString=mq://admin:passw0rd@localhost:1414/QM1?channel=DEV.ADMIN.SVRCONN&topicprefix=DEV" | Out-File -FilePath $Env:GITHUB_ENV -Encoding utf-8 -Append + echo "ServiceControl_TransportTests_IBMMQ_ConnectionString=mq://admin:passw0rd@localhost:1414/QM1?channel=DEV.ADMIN.SVRCONN&topicprefix=DEV" | Out-File -FilePath $Env:GITHUB_ENV -Encoding utf8 -Append - name: Setup SQS environment variables if: matrix.test-category == 'SQS' run: | - echo "AWS_REGION=${{ secrets.AWS_REGION }}" | Out-File -FilePath $Env:GITHUB_ENV -Encoding utf-8 -Append - echo "AWS_ACCESS_KEY_ID=${{ secrets.AWS_ACCESS_KEY_ID }}" | Out-File -FilePath $Env:GITHUB_ENV -Encoding utf-8 -Append - echo "AWS_SECRET_ACCESS_KEY=${{ secrets.AWS_SECRET_ACCESS_KEY }}" | Out-File -FilePath $Env:GITHUB_ENV -Encoding utf-8 -Append + echo "AWS_REGION=${{ secrets.AWS_REGION }}" | Out-File -FilePath $Env:GITHUB_ENV -Encoding utf8 -Append + echo "AWS_ACCESS_KEY_ID=${{ secrets.AWS_ACCESS_KEY_ID }}" | Out-File -FilePath $Env:GITHUB_ENV -Encoding utf8 -Append + echo "AWS_SECRET_ACCESS_KEY=${{ secrets.AWS_SECRET_ACCESS_KEY }}" | Out-File -FilePath $Env:GITHUB_ENV -Encoding utf8 -Append # Cleanup of queues starting with `GHA-` handled by https://github.com/Particular/NServiceBus.AmazonSQS/blob/master/.github/workflows/tests-cleanup.yml $connectString = "AccessKeyId=${{ secrets.AWS_ACCESS_KEY_ID }};SecretAccessKey=${{ secrets.AWS_SECRET_ACCESS_KEY }};Region=${{ secrets.AWS_REGION }};QueueNamePrefix=GHA-${{ github.run_id }}" - echo "ServiceControl_TransportTests_SQS_ConnectionString=$connectString" | Out-File -FilePath $Env:GITHUB_ENV -Encoding utf-8 -Append + echo "ServiceControl_TransportTests_SQS_ConnectionString=$connectString" | Out-File -FilePath $Env:GITHUB_ENV -Encoding utf8 -Append - name: Run tests - uses: Particular/run-tests-action@v1.7.0 + run: | + $projects = ./tools/ci-category-projects.ps1 -Category '${{ matrix.test-category }}' + ./tools/run-tests.ps1 -Projects ($projects -join "`n") env: ServiceControl_TESTS_FILTER: ${{ matrix.test-category }} PARTICULARSOFTWARE_LICENSE: ${{ secrets.LICENSETEXT }} AZURE_ACI_CREDENTIALS: ${{ secrets.AZURE_ACI_CREDENTIALS }} + cleanup-artifacts: + if: always() + needs: [ build-windows, build-linux, test ] + runs-on: ubuntu-latest + timeout-minutes: 30 + env: + AZURE_ACI_CREDENTIALS: ${{ secrets.AZURE_ACI_CREDENTIALS }} + AZURE_CI_RG: ServiceControl-CI + AZURE_CI_ACCOUNT: scpartpciartifacts + steps: + - name: Azure login + uses: azure/login@v3.0.1 + with: + creds: ${{ secrets.AZURE_ACI_CREDENTIALS }} + - name: Delete run containers + run: | + $key = az storage account keys list --account-name $env:AZURE_CI_ACCOUNT --resource-group $env:AZURE_CI_RG --query "[0].value" --output tsv + if (-not $key) { throw 'Could not obtain storage account key' } + "run-$($env:GITHUB_RUN_ID)-win", "run-$($env:GITHUB_RUN_ID)-linux" | ForEach-Object { + az storage container delete --account-name $env:AZURE_CI_ACCOUNT --account-key $key --name $_ --yes --only-show-errors + Write-Output "Deleted container $_" + } + windows-installers: uses: ./.github/workflows/build-windows.yml secrets: inherit @@ -179,4 +380,4 @@ jobs: container-test: needs: [ 'containers', 'db-container' ] uses: ./.github/workflows/container-integration-test.yml - secrets: inherit + secrets: inherit \ No newline at end of file diff --git a/.github/workflows/spike-lanes.yml b/.github/workflows/spike-lanes.yml deleted file mode 100644 index d8b7793899..0000000000 --- a/.github/workflows/spike-lanes.yml +++ /dev/null @@ -1,124 +0,0 @@ -# Spike: can we run more categories per OS runner using the new background/wait -# primitives? Windows-only, no cloud categories, no secrets required. -# -# Two jobs, same build, same five category groups (lanes), different scheduling: -# -# spike-serial : runs the lanes one after another on a single runner -# spike-lanes : runs the lanes as `background: true` steps, joined with wait-all -# -# What the run answers: -# 1. Machine minutes: serial >= lanes should collapse if the work is I/O bound. -# 2. 16 GB ceiling: do five acceptance lanes (ServiceControl hosts + RavenDB -# embedded servers each) fit, or do lanes OOM/swap? -# 3. Ports: lane-multi hosts primary+audit (33333/44444) while lane-raven-main -# and lane-audit host those same instance types concurrently. Bind failures -# tell us the acceptance harness uses fixed ports. -# -# Only for the ci_tryout branch. Intentionally not wired into the main matrix. - -name: Spike Lanes - -on: - workflow_dispatch: - push: - branches: - - ci_tryout - -concurrency: - group: spike-lanes-${{ github.ref }} - cancel-in-progress: true - -env: - PARTICULARSOFTWARE_LICENSE: ${{ secrets.LICENSETEXT }} - -defaults: - run: - shell: pwsh - -jobs: - spike-serial: - name: Spike Serial (baseline) - runs-on: windows-latest - timeout-minutes: 90 - steps: - - name: Checkout - uses: actions/checkout@v7.0.1 - - name: Setup .NET SDK - uses: actions/setup-dotnet@v6.0.0 - with: - global-json-file: global.json - - name: Download RavenDB Server - run: ./tools/download-ravendb-server.ps1 - - name: Build - run: dotnet build src --configuration Release -graph - - name: Run lanes sequentially - run: | - ./tools/spike-run-lane.ps1 -Name raven-main -Filter PrimaryRavenAcceptance -Projects @( - 'src/ServiceControl.AcceptanceTests.RavenDB/ServiceControl.AcceptanceTests.RavenDB.csproj') - ./tools/spike-run-lane.ps1 -Name audit -Filter Default -Projects @( - 'src/ServiceControl.Audit.AcceptanceTests/ServiceControl.Audit.AcceptanceTests.csproj', - 'src/ServiceControl.Audit.AcceptanceTests.RavenDB/ServiceControl.Audit.AcceptanceTests.RavenDB.csproj') - ./tools/spike-run-lane.ps1 -Name multi -Filter Default -Projects @( - 'src/ServiceControl.MultiInstance.AcceptanceTests/ServiceControl.MultiInstance.AcceptanceTests.csproj') - ./tools/spike-run-lane.ps1 -Name monitor -Filter Default -Projects @( - 'src/ServiceControl.Monitoring.AcceptanceTests/ServiceControl.Monitoring.AcceptanceTests.csproj', - 'src/ServiceControl.Monitoring.UnitTests/ServiceControl.Monitoring.UnitTests.csproj') - ./tools/spike-run-lane.ps1 -Name unit -Filter Default -Projects @( - 'src/ServiceControl.UnitTests/ServiceControl.UnitTests.csproj', - 'src/ServiceControl.Audit.UnitTests/ServiceControl.Audit.UnitTests.csproj', - 'src/ServiceControl.Config.Tests/ServiceControl.Config.Tests.csproj', - 'src/ServiceControl.Infrastructure.Tests/ServiceControl.Infrastructure.Tests.csproj', - 'src/Particular.LicensingComponent.UnitTests/Particular.LicensingComponent.UnitTests.csproj', - 'src/ServiceControl.Persistence.Tests.InMemory/ServiceControl.Persistence.Tests.InMemory.csproj', - 'src/ServiceControl.Audit.Persistence.Tests/ServiceControl.Audit.Persistence.Tests.csproj', - 'src/ServiceControl.Audit.Persistence.Tests.RavenDB/ServiceControl.Audit.Persistence.Tests.RavenDB.csproj', - 'src/ServiceControlInstaller.Engine.UnitTests/ServiceControlInstaller.Engine.UnitTests.csproj', - 'src/ServiceControlInstaller.Packaging.UnitTests/ServiceControlInstaller.Packaging.UnitTests.csproj', - 'src/ServiceControl.Transports.Tests/ServiceControl.Transports.Tests.csproj') - - spike-lanes: - name: Spike Lanes (parallel) - runs-on: windows-latest - timeout-minutes: 90 - steps: - - name: Checkout - uses: actions/checkout@v7.0.1 - - name: Setup .NET SDK - uses: actions/setup-dotnet@v6.0.0 - with: - global-json-file: global.json - - name: Download RavenDB Server - run: ./tools/download-ravendb-server.ps1 - - name: Build - run: dotnet build src --configuration Release -graph - - name: Memory sampler - id: memory - run: ./tools/spike-memory.ps1 - background: true - - name: Lane raven-main (hosts main SC, 33333) - id: lane-raven-main - run: ./tools/spike-run-lane.ps1 -Name raven-main -Filter PrimaryRavenAcceptance -Projects @('src/ServiceControl.AcceptanceTests.RavenDB/ServiceControl.AcceptanceTests.RavenDB.csproj') - background: true - - name: Lane audit (hosts audit SC, 44444) - id: lane-audit - run: ./tools/spike-run-lane.ps1 -Name audit -Filter Default -Projects @('src/ServiceControl.Audit.AcceptanceTests/ServiceControl.Audit.AcceptanceTests.csproj', 'src/ServiceControl.Audit.AcceptanceTests.RavenDB/ServiceControl.Audit.AcceptanceTests.RavenDB.csproj') - background: true - - name: Lane multi (hosts primary AND audit) - id: lane-multi - run: ./tools/spike-run-lane.ps1 -Name multi -Filter Default -Projects @('src/ServiceControl.MultiInstance.AcceptanceTests/ServiceControl.MultiInstance.AcceptanceTests.csproj') - background: true - - name: Lane monitor (hosts monitoring SC, 33633) - id: lane-monitor - run: ./tools/spike-run-lane.ps1 -Name monitor -Filter Default -Projects @('src/ServiceControl.Monitoring.AcceptanceTests/ServiceControl.Monitoring.AcceptanceTests.csproj', 'src/ServiceControl.Monitoring.UnitTests/ServiceControl.Monitoring.UnitTests.csproj') - background: true - - name: Lane unit (no fixed ports) - id: lane-unit - run: ./tools/spike-run-lane.ps1 -Name unit -Filter Default -Projects @('src/ServiceControl.UnitTests/ServiceControl.UnitTests.csproj', 'src/ServiceControl.Audit.UnitTests/ServiceControl.Audit.UnitTests.csproj', 'src/ServiceControl.Config.Tests/ServiceControl.Config.Tests.csproj', 'src/ServiceControl.Infrastructure.Tests/ServiceControl.Infrastructure.Tests.csproj', 'src/Particular.LicensingComponent.UnitTests/Particular.LicensingComponent.UnitTests.csproj', 'src/ServiceControl.Persistence.Tests.InMemory/ServiceControl.Persistence.Tests.InMemory.csproj', 'src/ServiceControl.Audit.Persistence.Tests/ServiceControl.Audit.Persistence.Tests.csproj', 'src/ServiceControl.Audit.Persistence.Tests.RavenDB/ServiceControl.Audit.Persistence.Tests.RavenDB.csproj', 'src/ServiceControlInstaller.Engine.UnitTests/ServiceControlInstaller.Engine.UnitTests.csproj', 'src/ServiceControlInstaller.Packaging.UnitTests/ServiceControlInstaller.Packaging.UnitTests.csproj', 'src/ServiceControl.Transports.Tests/ServiceControl.Transports.Tests.csproj') - background: true - - name: Wait for lanes - wait-all: - - name: Memory summary - if: always() - run: | - Get-Content -Path .\spike-memory.csv -ErrorAction SilentlyContinue | Select-Object -First 3 - Get-Content -Path .\spike-memory.csv -ErrorAction SilentlyContinue | Select-Object -Last 10 \ No newline at end of file diff --git a/tools/ci-category-projects.ps1 b/tools/ci-category-projects.ps1 new file mode 100644 index 0000000000..94b7fcde22 --- /dev/null +++ b/tools/ci-category-projects.ps1 @@ -0,0 +1,62 @@ +# Category -> test project mapping for the build-once CI workflow. +# Mirrors the assembly-level IncludeIn*Tests attributes in src/TestHelper. +# Used by the build job to stage test outputs and by test jobs to download +# their category's closure and run it. + +param( + [Parameter(Mandatory)] + [string]$Category, + + [switch]$ListCategories +) + +$ErrorActionPreference = 'Stop' + +$map = @{ + 'Default' = @( + 'ServiceControl.UnitTests', + 'ServiceControl.Audit.UnitTests', + 'ServiceControl.Config.Tests', + 'ServiceControl.Infrastructure.Tests', + 'Particular.LicensingComponent.UnitTests', + 'ServiceControl.Persistence.Tests.InMemory', + 'ServiceControl.Audit.Persistence.Tests', + 'ServiceControl.Audit.Persistence.Tests.RavenDB', + 'ServiceControlInstaller.Engine.UnitTests', + 'ServiceControlInstaller.Packaging.UnitTests', + 'ServiceControl.Transports.Tests', + 'ServiceControl.Audit.AcceptanceTests', + 'ServiceControl.Audit.AcceptanceTests.RavenDB', + 'ServiceControl.Monitoring.UnitTests', + 'ServiceControl.Monitoring.AcceptanceTests', + 'ServiceControl.MultiInstance.AcceptanceTests' + ) + 'SqlServer' = @('ServiceControl.Transports.SqlServer.Tests') + 'SqlServerPersistence' = @('ServiceControl.Persistence.Tests.SqlServer') + 'AzureServiceBus' = @('ServiceControl.Transports.ASBS.Tests') + 'RabbitMQ' = @( + 'ServiceControl.Transports.RabbitMQClassicConventionalRouting.Tests', + 'ServiceControl.Transports.RabbitMQClassicDirectRouting.Tests', + 'ServiceControl.Transports.RabbitMQQuorumConventionalRouting.Tests', + 'ServiceControl.Transports.RabbitMQQuorumDirectRouting.Tests' + ) + 'AzureStorageQueues' = @('ServiceControl.Transports.ASQ.Tests') + 'MSMQ' = @('ServiceControl.Transports.Msmq.Tests') + 'SQS' = @('ServiceControl.Transports.SQS.Tests') + 'PrimaryRavenAcceptance' = @('ServiceControl.AcceptanceTests.RavenDB') + 'PrimaryRavenPersistence'= @('ServiceControl.Persistence.Tests.RavenDB') + 'PostgreSQL' = @('ServiceControl.Transports.PostgreSql.Tests') + 'PostgreSQLPersistence' = @('ServiceControl.Persistence.Tests.PostgreSql') + 'IBMMQ' = @('ServiceControl.Transports.IBMMQ.Tests') +} + +if ($ListCategories) { + $map.Keys | Sort-Object + exit 0 +} + +if (-not $map.ContainsKey($Category)) { + throw "Unknown category '$Category'. Known: $($map.Keys -join ', ')" +} + +$map[$Category] | ForEach-Object { "src/$_" } \ No newline at end of file diff --git a/tools/run-tests.ps1 b/tools/run-tests.ps1 new file mode 100644 index 0000000000..dcf374032c --- /dev/null +++ b/tools/run-tests.ps1 @@ -0,0 +1,54 @@ +# Runs dotnet test against an explicit list of test projects with --no-build, +# because the build already happened in the build-once CI job and the outputs +# were downloaded. Mirrors what Particular/run-tests-action does. + +param( + [Parameter(Mandatory)] + [string]$Projects, + + [string]$TargetPlatform = 'x64' +) + +$ErrorActionPreference = 'Stop' + +$projectPaths = $Projects -split "`n" | ForEach-Object { $_.Trim() } | Where-Object { $_ } + +if ($projectPaths.Count -eq 0) { + throw 'No test projects were supplied.' +} + +Write-Output "Target Platform = $TargetPlatform" + +$isUnix = $PSVersionTable.Platform -eq 'Unix' +$exitCode = 0 + +foreach ($project in $projectPaths) { + $frameworks = @( + (Select-Xml -Path $project -XPath "/Project/PropertyGroup/TargetFramework").Node.InnerText + (Select-Xml -Path $project -XPath "/Project/PropertyGroup/TargetFrameworks").Node.InnerText -split ';' + ) | Where-Object { $_ } + + if ($frameworks.Count -eq 0) { + throw "Could not determine a target framework for $project." + } + + foreach ($framework in $frameworks) { + if ($isUnix -and ($framework.StartsWith('net4') -or $framework.Contains('-windows'))) { + Write-Output "Skipping $(Split-Path $project -Leaf) ($framework) because it cannot run on this platform." + continue + } + + Write-Output "::group::Running $(Split-Path $project -Leaf) ($framework)" + + dotnet test $project --configuration Release --no-build --no-restore --framework $framework --logger "GitHubActions;report-warnings=false" -- RunConfiguration.TreatNoTestsAsError=true "RunConfiguration.TargetPlatform=$TargetPlatform" + + Write-Output '::endgroup::' + + if ($LASTEXITCODE -ne 0) { + Write-Output "::error::Exit code = $LASTEXITCODE" + $exitCode = 1 + } + } +} + +exit $exitCode \ No newline at end of file diff --git a/tools/spike-memory.ps1 b/tools/spike-memory.ps1 deleted file mode 100644 index 9c504bf6c8..0000000000 --- a/tools/spike-memory.ps1 +++ /dev/null @@ -1,38 +0,0 @@ -# Spike support script for the "background lanes" CI experiment. -# Samples available physical memory every 5 seconds and writes a CSV, so we can -# see whether concurrent acceptance-test lanes fit in the 16 GB runner. Stops -# early once every lane (5 sentinel-* files) has finished, or after MaxSeconds. - -param( - [int]$MaxSeconds = 1800 -) - -$log = 'spike-memory.csv' -$start = Get-Date -$minAvailableGb = [double]::MaxValue -$samples = 0 - -while (((Get-Date) - $start).TotalSeconds -lt $MaxSeconds) { - try { - $availableGb = (Get-CimInstance Win32_OperatingSystem).FreePhysicalMemory / 1MB - } - catch { - $availableGb = -1 - } - - "$(Get-Date -Format HH:mm:ss),$([math]::Round($availableGb, 1))" | Add-Content -Path $log - if ($availableGb -ge 0 -and $availableGb -lt $minAvailableGb) { - $minAvailableGb = $availableGb - } - $samples++ - - $doneLanes = @(Get-ChildItem -Path . -Filter 'sentinel-*' -ErrorAction SilentlyContinue) - if ($doneLanes.Count -ge 5) { - break - } - - Start-Sleep -Seconds 5 -} - -Write-Output "Memory samples: $samples, min available: $([math]::Round($minAvailableGb, 1)) GB" -Get-Content -Path $log | Select-Object -Last 5 \ No newline at end of file diff --git a/tools/spike-run-lane.ps1 b/tools/spike-run-lane.ps1 deleted file mode 100644 index 87a0bc37b3..0000000000 --- a/tools/spike-run-lane.ps1 +++ /dev/null @@ -1,36 +0,0 @@ -# Spike support script for the "background lanes" CI experiment. -# Runs one test category lane: an explicit list of test projects with the -# ServiceControl_TESTS_FILTER env var set, mirroring what run-tests-action does -# on master. Touches a sentinel file when done so the memory sampler can stop. -# Exit code reflects any failing project, but the sentinel is written either way. - -param( - [Parameter(Mandatory)] - [string]$Name, - - [Parameter(Mandatory)] - [string[]]$Projects, - - [string]$Filter = 'Default' -) - -$ErrorActionPreference = 'Continue' - -$env:ServiceControl_TESTS_FILTER = $Filter -Write-Output "Lane '$Name' filter='$Filter' projects=$($Projects.Count)" - -$exitCode = 0 - -foreach ($project in $Projects) { - Write-Output "::group::$Name :: $project" - dotnet test $project --configuration Release --no-build --logger "GitHubActions" - if ($LASTEXITCODE -ne 0) { - Write-Output "::error::$Name :: $project exited with $LASTEXITCODE" - $exitCode = 1 - } - Write-Output "::endgroup::" -} - -New-Item -ItemType File -Path "sentinel-$Name" -Force | Out-Null - -exit $exitCode \ No newline at end of file From 0a55fd4eae02c0bfbd8aa746f19ff7461285acaa Mon Sep 17 00:00:00 2001 From: Daniel Marbach Date: Sun, 9 Aug 2026 00:45:13 +0200 Subject: [PATCH 3/3] CI: fix validation - per-OS test jobs with static needs (no dynamic expressions in needs) --- .github/workflows/ci.yml | 151 +++++++++++++++++++++++++++++++++------ 1 file changed, 129 insertions(+), 22 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a62fc8e767..4abddfa987 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -198,29 +198,136 @@ jobs: "container=$container" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append "sas=$sas" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append - test: - name: ${{ matrix.os-name }}-${{ matrix.test-category }} - needs: ${{ matrix.os-name == 'Windows' && 'build-windows' || 'build-linux' }} - runs-on: ${{ matrix.os }} + test-windows: + name: Windows-${{ matrix.test-category }} + needs: build-windows + runs-on: windows-latest + timeout-minutes: 90 + env: + AZURE_CI_ACCOUNT: ${{ needs.build-windows.outputs.account }} + AZURE_CI_CONTAINER: ${{ needs.build-windows.outputs.container }} + AZURE_CI_SAS: ${{ needs.build-windows.outputs.sas }} + strategy: + matrix: + test-category: [ Default, SqlServer, SqlServerPersistence, AzureServiceBus, RabbitMQ, AzureStorageQueues, MSMQ, SQS, PrimaryRavenAcceptance, PrimaryRavenPersistence, PostgreSQL, PostgreSQLPersistence ] + fail-fast: false + steps: + - name: Check for secrets + env: + SECRETS_AVAILABLE: ${{ secrets.SECRETS_AVAILABLE }} + run: exit $(If ($env:SECRETS_AVAILABLE -eq 'true') { 0 } Else { 1 }) + - name: Checkout + uses: actions/checkout@v7.0.1 + with: + fetch-depth: 0 + - name: Setup .NET SDK + uses: actions/setup-dotnet@v6.0.0 + with: + global-json-file: global.json + - parallel: + - name: Setup WSL + uses: Particular/setup-wsl-action@v1.1.0 + - name: Download RavenDB Server + run: ./tools/download-ravendb-server.ps1 + - name: Download test outputs + run: | + $projects = ./tools/ci-category-projects.ps1 -Category '${{ matrix.test-category }}' + $projects | ForEach-Object { + Write-Output "Downloading $_" + az storage blob download-batch --account-name $env:AZURE_CI_ACCOUNT --sas-token $env:AZURE_CI_SAS --destination $env:GITHUB_WORKSPACE --source $env:AZURE_CI_CONTAINER --pattern "$_/**" --output none + if ($LASTEXITCODE -ne 0) { throw "Download failed for $_" } + } + - name: Setup SQL Server + uses: Particular/install-sql-server-action@v3.0.0 + if: matrix.test-category == 'SqlServer' + with: + connection-string-env-var: ServiceControl_TransportTests_SQL_ConnectionString + catalog: nservicebus + - name: Setup SQL Server persistence + uses: Particular/install-sql-server-action@v3.0.0 + if: matrix.test-category == 'SqlServerPersistence' + with: + connection-string-env-var: ServiceControl_Persistence_SqlServer_ConnectionString + catalog: ServiceControl + enable-full-text-search: true + - name: Setup PostgreSQL + uses: Particular/setup-postgres-action@v3.0.0 + if: matrix.test-category == 'PostgreSQL' + with: + connection-string-name: ServiceControl_TransportTests_PostgreSQL_ConnectionString + registry-username: ${{ secrets.DOCKERHUB_USERNAME }} + registry-password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Setup PostgreSQL persistence + uses: Particular/setup-postgres-action@v3.0.0 + if: matrix.test-category == 'PostgreSQLPersistence' + with: + connection-string-name: ServiceControl_Persistence_PostgreSql_ConnectionString + registry-username: ${{ secrets.DOCKERHUB_USERNAME }} + registry-password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Setup RabbitMQ + uses: Particular/setup-rabbitmq-action@v2.0.0 + if: matrix.test-category == 'RabbitMQ' + with: + connection-string-name: ServiceControl_TransportTests_RabbitMQ_ConnectionString + registry-username: ${{ secrets.DOCKERHUB_USERNAME }} + registry-password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Setup Azure Service Bus + uses: Particular/setup-azureservicebus-action@v2.5.0 + if: matrix.test-category == 'AzureServiceBus' + with: + connection-string-name: ServiceControl_TransportTests_ASBS_ConnectionString + azure-credentials: ${{ secrets.AZURE_ACI_CREDENTIALS }} + tag: ServiceControl + - name: Setup Azure Storage Queues + uses: Particular/setup-azurestorage-action@v2.0.0 + if: matrix.test-category == 'AzureStorageQueues' + with: + connection-string-name: ServiceControl_TransportTests_ASQ_ConnectionString + azure-credentials: ${{ secrets.AZURE_ACI_CREDENTIALS }} + tag: ServiceControl + - name: Setup IBM MQ + if: matrix.test-category == 'IBMMQ' + run: | + docker run --name ibmmq -d -p 1414:1414 -p 9443:9443 ` + --health-cmd "dspmq" --health-interval 10s --health-timeout 5s --health-retries 10 --health-start-period 30s ` + -e LICENSE=accept -e MQ_QMGR_NAME=QM1 -e MQ_ADMIN_PASSWORD=passw0rd ` + icr.io/ibm-messaging/mq:latest + # Wait for container health check to pass + while ((docker inspect --format '{{.State.Health.Status}}' ibmmq) -ne 'healthy') { + Start-Sleep -Seconds 2 + } + echo "ServiceControl_TransportTests_IBMMQ_ConnectionString=mq://admin:passw0rd@localhost:1414/QM1?channel=DEV.ADMIN.SVRCONN&topicprefix=DEV" | Out-File -FilePath $Env:GITHUB_ENV -Encoding utf8 -Append + - name: Setup SQS environment variables + if: matrix.test-category == 'SQS' + run: | + echo "AWS_REGION=${{ secrets.AWS_REGION }}" | Out-File -FilePath $Env:GITHUB_ENV -Encoding utf8 -Append + echo "AWS_ACCESS_KEY_ID=${{ secrets.AWS_ACCESS_KEY_ID }}" | Out-File -FilePath $Env:GITHUB_ENV -Encoding utf8 -Append + echo "AWS_SECRET_ACCESS_KEY=${{ secrets.AWS_SECRET_ACCESS_KEY }}" | Out-File -FilePath $Env:GITHUB_ENV -Encoding utf8 -Append + + # Cleanup of queues starting with `GHA-` handled by https://github.com/Particular/NServiceBus.AmazonSQS/blob/master/.github/workflows/tests-cleanup.yml + $connectString = "AccessKeyId=${{ secrets.AWS_ACCESS_KEY_ID }};SecretAccessKey=${{ secrets.AWS_SECRET_ACCESS_KEY }};Region=${{ secrets.AWS_REGION }};QueueNamePrefix=GHA-${{ github.run_id }}" + echo "ServiceControl_TransportTests_SQS_ConnectionString=$connectString" | Out-File -FilePath $Env:GITHUB_ENV -Encoding utf8 -Append + - name: Run tests + run: | + $projects = ./tools/ci-category-projects.ps1 -Category '${{ matrix.test-category }}' + ./tools/run-tests.ps1 -Projects ($projects -join "`n") + env: + ServiceControl_TESTS_FILTER: ${{ matrix.test-category }} + PARTICULARSOFTWARE_LICENSE: ${{ secrets.LICENSETEXT }} + AZURE_ACI_CREDENTIALS: ${{ secrets.AZURE_ACI_CREDENTIALS }} + + test-linux: + name: Linux-${{ matrix.test-category }} + needs: build-linux + runs-on: ubuntu-latest timeout-minutes: 90 env: - AZURE_CI_ACCOUNT: ${{ needs[format('build-{0}', lower(matrix.os-name))].outputs.account }} - AZURE_CI_CONTAINER: ${{ needs[format('build-{0}', lower(matrix.os-name))].outputs.container }} - AZURE_CI_SAS: ${{ needs[format('build-{0}', lower(matrix.os-name))].outputs.sas }} + AZURE_CI_ACCOUNT: ${{ needs.build-linux.outputs.account }} + AZURE_CI_CONTAINER: ${{ needs.build-linux.outputs.container }} + AZURE_CI_SAS: ${{ needs.build-linux.outputs.sas }} strategy: matrix: - os: [windows-latest, ubuntu-latest] - test-category: [ Default, SqlServer, SqlServerPersistence, AzureServiceBus, RabbitMQ, AzureStorageQueues, MSMQ, SQS, PrimaryRavenAcceptance, PrimaryRavenPersistence, PostgreSQL, PostgreSQLPersistence, IBMMQ ] - include: - - os: windows-latest - os-name: Windows - - os: ubuntu-latest - os-name: Linux - exclude: - - os: ubuntu-latest - test-category: MSMQ - - os: windows-latest - test-category: IBMMQ + test-category: [ Default, SqlServer, SqlServerPersistence, AzureServiceBus, RabbitMQ, AzureStorageQueues, SQS, PrimaryRavenAcceptance, PrimaryRavenPersistence, PostgreSQL, PostgreSQLPersistence, IBMMQ ] fail-fast: false steps: - name: Check for secrets @@ -253,12 +360,12 @@ jobs: # once the issue is resolved it should be able to be re-floated # https://github.com/Azure/azure-cli/issues/32980 - name: Set Python 3.13 (Linux) - if: matrix.os-name == 'Linux' && matrix.test-category == 'AzureServiceBus' + if: matrix.test-category == 'AzureServiceBus' uses: actions/setup-python@v5 with: python-version: '3.13' - name: Install pinned Azure CLI on Python 3.13 (Linux) - if: matrix.os-name == 'Linux' && matrix.test-category == 'AzureServiceBus' + if: matrix.test-category == 'AzureServiceBus' shell: bash run: | python -m pip install --upgrade pip @@ -346,7 +453,7 @@ jobs: cleanup-artifacts: if: always() - needs: [ build-windows, build-linux, test ] + needs: [ build-windows, build-linux, test-windows, test-linux ] runs-on: ubuntu-latest timeout-minutes: 30 env: