From fd0c9e112262d3103eec01037a2f5c89020978b1 Mon Sep 17 00:00:00 2001 From: John Simons Date: Sat, 8 Aug 2026 14:21:47 +1000 Subject: [PATCH 1/8] Optimize CI by categorizing tests and parallelizing builds This refactors the CI workflow to improve efficiency and reduce execution time. Test projects are now assigned `TestCategory` properties, enabling matrix jobs to dynamically select, build, and run only the relevant projects for their category. This significantly reduces build times and resource consumption. A new `compile` job ensures the entire `src` directory remains buildable. The build step for test jobs is now backgrounded to overlap with infrastructure setup, further improving parallelization. Also corrects inconsistent `PostgreSQL` casing to `PostgreSql` in the workflow. --- .github/workflows/ci.yml | 85 ++++++++++++------ .gitignore | 2 + ...icular.LicensingComponent.UnitTests.csproj | 1 + ...viceControl.AcceptanceTests.RavenDB.csproj | 1 + ...ntrol.Audit.AcceptanceTests.RavenDB.csproj | 1 + ...erviceControl.Audit.AcceptanceTests.csproj | 1 + ...rol.Audit.Persistence.Tests.RavenDB.csproj | 1 + ...viceControl.Audit.Persistence.Tests.csproj | 1 + .../ServiceControl.Audit.UnitTests.csproj | 1 + .../ServiceControl.Config.Tests.csproj | 1 + ...ServiceControl.Infrastructure.Tests.csproj | 1 + ...eControl.Monitoring.AcceptanceTests.csproj | 1 + ...ServiceControl.Monitoring.UnitTests.csproj | 1 + ...ntrol.MultiInstance.AcceptanceTests.csproj | 1 + ...eControl.Persistence.Tests.InMemory.csproj | 1 + ...ontrol.Persistence.Tests.PostgreSql.csproj | 1 + ...ceControl.Persistence.Tests.RavenDB.csproj | 1 + ...Control.Persistence.Tests.SqlServer.csproj | 1 + ...erviceControl.Transports.ASBS.Tests.csproj | 1 + ...ServiceControl.Transports.ASQ.Tests.csproj | 3 +- ...rviceControl.Transports.IBMMQ.Tests.csproj | 1 + ...erviceControl.Transports.Msmq.Tests.csproj | 1 + ...Control.Transports.PostgreSql.Tests.csproj | 1 + ...itMQClassicConventionalRoutingTests.csproj | 3 +- ....RabbitMQClassicDirectRouting.Tests.csproj | 3 +- ...itMQQuorumConventionalRouting.Tests.csproj | 3 +- ...s.RabbitMQQuorumDirectRouting.Tests.csproj | 3 +- ...ServiceControl.Transports.SQS.Tests.csproj | 1 + ...eControl.Transports.SqlServer.Tests.csproj | 1 + .../ServiceControl.Transports.Tests.csproj | 1 + .../ServiceControl.UnitTests.csproj | 1 + ...ceControlInstaller.Engine.UnitTests.csproj | 1 + ...ontrolInstaller.Packaging.UnitTests.csproj | 1 + tools/run-tests.ps1 | 60 +++++++++++++ tools/select-test-projects.ps1 | 89 +++++++++++++++++++ 35 files changed, 244 insertions(+), 33 deletions(-) create mode 100644 tools/run-tests.ps1 create mode 100644 tools/select-test-projects.ps1 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5cf6b62282..f1e3a398b8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,7 +18,7 @@ jobs: strategy: matrix: os: [windows-latest, ubuntu-latest] - test-category: [ Default, SqlServer, SqlServerPersistence, AzureServiceBus, RabbitMQ, AzureStorageQueues, MSMQ, SQS, PrimaryRavenAcceptance, PrimaryRavenPersistence, PostgreSQL, PostgreSQLPersistence, IBMMQ ] + test-category: [ Default, SqlServer, SqlServerPersistence, AzureServiceBus, RabbitMQ, AzureStorageQueues, MSMQ, SQS, PrimaryRavenAcceptance, PrimaryRavenPersistence, PostgreSql, PostgreSqlPersistence, IBMMQ ] include: - os: windows-latest os-name: Windows @@ -43,30 +43,24 @@ jobs: uses: actions/setup-dotnet@v6.0.0 with: global-json-file: global.json - - parallel: - - name: Setup WSL - uses: Particular/setup-wsl-action@v1.0.0 - - 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 + - name: Select test projects + id: select + run: ./tools/select-test-projects.ps1 -Category ${{ matrix.test-category }} + # ServiceControlInstaller.Packaging is only in the Default category's build closure, and its zip + # targets fail without a self-contained server in deploy/. Tests never read it: RavenDB.Embedded + # supplies its own server into each test project's output directory. + - name: Download RavenDB Server 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 + run: ./tools/download-ravendb-server.ps1 + # Backgrounded so that it overlaps the infrastructure steps below. Those steps are an ordered + # chain (Setup WSL provisions the Docker host the database actions rely on), so they stay in the + # foreground and the job joins the build at the `wait` before running tests. + - name: Build + id: build + background: true + run: dotnet build tests.proj --configuration Release -graph + - name: Setup WSL + uses: Particular/setup-wsl-action@v1.0.0 # 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 @@ -86,7 +80,7 @@ jobs: - 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' + 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 @@ -104,14 +98,14 @@ jobs: enable-full-text-search: true - name: Setup PostgreSQL uses: Particular/setup-postgres-action@v3.0.0 - if: matrix.test-category == 'PostgreSQL' + 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' + if: matrix.test-category == 'PostgreSqlPersistence' with: connection-string-name: ServiceControl_Persistence_PostgreSql_ConnectionString registry-username: ${{ secrets.DOCKERHUB_USERNAME }} @@ -160,13 +154,48 @@ jobs: # 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 + - name: Wait for build + wait: build - name: Run tests - uses: Particular/run-tests-action@v1.7.0 + run: ./tools/run-tests.ps1 -Projects $Env:TEST_PROJECTS env: + TEST_PROJECTS: ${{ steps.select.outputs.test-projects }} ServiceControl_TESTS_FILTER: ${{ matrix.test-category }} PARTICULARSOFTWARE_LICENSE: ${{ secrets.LICENSETEXT }} AZURE_ACI_CREDENTIALS: ${{ secrets.AZURE_ACI_CREDENTIALS }} + # The test matrix only builds each category's own dependency closure, so this job is what keeps + # projects outside every closure (HealthCheckApp, Particular.PlatformSample.ServiceControl, + # LegacyArtifacts) compiling. It runs alongside the matrix and is not on its critical path. + compile: + name: ${{ matrix.os-name }}-Compile + runs-on: ${{ matrix.os }} + strategy: + matrix: + include: + - os: windows-latest + os-name: Windows + - os: ubuntu-latest + os-name: Linux + 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 + - name: Download RavenDB Server + run: ./tools/download-ravendb-server.ps1 + - name: Build + run: dotnet build src --configuration Release -graph + windows-installers: uses: ./.github/workflows/build-windows.yml secrets: inherit diff --git a/.gitignore b/.gitignore index 1653797127..b3b621401f 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,8 @@ /binaries /deploy /nugets +# Generated by tools/select-test-projects.ps1 +/tests.proj build32 *.vshost.* .nu diff --git a/src/Particular.LicensingComponent.UnitTests/Particular.LicensingComponent.UnitTests.csproj b/src/Particular.LicensingComponent.UnitTests/Particular.LicensingComponent.UnitTests.csproj index b0094aab94..4c62b01f22 100644 --- a/src/Particular.LicensingComponent.UnitTests/Particular.LicensingComponent.UnitTests.csproj +++ b/src/Particular.LicensingComponent.UnitTests/Particular.LicensingComponent.UnitTests.csproj @@ -2,6 +2,7 @@ net10.0 + Default diff --git a/src/ServiceControl.AcceptanceTests.RavenDB/ServiceControl.AcceptanceTests.RavenDB.csproj b/src/ServiceControl.AcceptanceTests.RavenDB/ServiceControl.AcceptanceTests.RavenDB.csproj index 0cf6c7efa5..26650ebe99 100644 --- a/src/ServiceControl.AcceptanceTests.RavenDB/ServiceControl.AcceptanceTests.RavenDB.csproj +++ b/src/ServiceControl.AcceptanceTests.RavenDB/ServiceControl.AcceptanceTests.RavenDB.csproj @@ -2,6 +2,7 @@ net10.0 + PrimaryRavenAcceptance diff --git a/src/ServiceControl.Audit.AcceptanceTests.RavenDB/ServiceControl.Audit.AcceptanceTests.RavenDB.csproj b/src/ServiceControl.Audit.AcceptanceTests.RavenDB/ServiceControl.Audit.AcceptanceTests.RavenDB.csproj index 223e117c20..b6da0faf58 100644 --- a/src/ServiceControl.Audit.AcceptanceTests.RavenDB/ServiceControl.Audit.AcceptanceTests.RavenDB.csproj +++ b/src/ServiceControl.Audit.AcceptanceTests.RavenDB/ServiceControl.Audit.AcceptanceTests.RavenDB.csproj @@ -2,6 +2,7 @@ net10.0 + Default diff --git a/src/ServiceControl.Audit.AcceptanceTests/ServiceControl.Audit.AcceptanceTests.csproj b/src/ServiceControl.Audit.AcceptanceTests/ServiceControl.Audit.AcceptanceTests.csproj index 2bbf59bc45..0d59ffe0ef 100644 --- a/src/ServiceControl.Audit.AcceptanceTests/ServiceControl.Audit.AcceptanceTests.csproj +++ b/src/ServiceControl.Audit.AcceptanceTests/ServiceControl.Audit.AcceptanceTests.csproj @@ -2,6 +2,7 @@ net10.0 + Default diff --git a/src/ServiceControl.Audit.Persistence.Tests.RavenDB/ServiceControl.Audit.Persistence.Tests.RavenDB.csproj b/src/ServiceControl.Audit.Persistence.Tests.RavenDB/ServiceControl.Audit.Persistence.Tests.RavenDB.csproj index 7b0f2fbb6a..1d1e5d2635 100644 --- a/src/ServiceControl.Audit.Persistence.Tests.RavenDB/ServiceControl.Audit.Persistence.Tests.RavenDB.csproj +++ b/src/ServiceControl.Audit.Persistence.Tests.RavenDB/ServiceControl.Audit.Persistence.Tests.RavenDB.csproj @@ -2,6 +2,7 @@ net10.0 + Default diff --git a/src/ServiceControl.Audit.Persistence.Tests/ServiceControl.Audit.Persistence.Tests.csproj b/src/ServiceControl.Audit.Persistence.Tests/ServiceControl.Audit.Persistence.Tests.csproj index b7f6909c1f..8d02310f98 100644 --- a/src/ServiceControl.Audit.Persistence.Tests/ServiceControl.Audit.Persistence.Tests.csproj +++ b/src/ServiceControl.Audit.Persistence.Tests/ServiceControl.Audit.Persistence.Tests.csproj @@ -2,6 +2,7 @@ net10.0 + Default diff --git a/src/ServiceControl.Audit.UnitTests/ServiceControl.Audit.UnitTests.csproj b/src/ServiceControl.Audit.UnitTests/ServiceControl.Audit.UnitTests.csproj index ec6be43716..0b26c9f9a0 100644 --- a/src/ServiceControl.Audit.UnitTests/ServiceControl.Audit.UnitTests.csproj +++ b/src/ServiceControl.Audit.UnitTests/ServiceControl.Audit.UnitTests.csproj @@ -2,6 +2,7 @@ net10.0 + Default diff --git a/src/ServiceControl.Config.Tests/ServiceControl.Config.Tests.csproj b/src/ServiceControl.Config.Tests/ServiceControl.Config.Tests.csproj index 8bafc4ae31..17ca280f67 100644 --- a/src/ServiceControl.Config.Tests/ServiceControl.Config.Tests.csproj +++ b/src/ServiceControl.Config.Tests/ServiceControl.Config.Tests.csproj @@ -2,6 +2,7 @@ net10.0-windows10.0.19041.0 + Default x64 true diff --git a/src/ServiceControl.Infrastructure.Tests/ServiceControl.Infrastructure.Tests.csproj b/src/ServiceControl.Infrastructure.Tests/ServiceControl.Infrastructure.Tests.csproj index 7664d8aa63..8feaa7df5a 100644 --- a/src/ServiceControl.Infrastructure.Tests/ServiceControl.Infrastructure.Tests.csproj +++ b/src/ServiceControl.Infrastructure.Tests/ServiceControl.Infrastructure.Tests.csproj @@ -2,6 +2,7 @@ net10.0 + Default diff --git a/src/ServiceControl.Monitoring.AcceptanceTests/ServiceControl.Monitoring.AcceptanceTests.csproj b/src/ServiceControl.Monitoring.AcceptanceTests/ServiceControl.Monitoring.AcceptanceTests.csproj index 19e62962ab..54b53b33e5 100644 --- a/src/ServiceControl.Monitoring.AcceptanceTests/ServiceControl.Monitoring.AcceptanceTests.csproj +++ b/src/ServiceControl.Monitoring.AcceptanceTests/ServiceControl.Monitoring.AcceptanceTests.csproj @@ -2,6 +2,7 @@ net10.0 + Default diff --git a/src/ServiceControl.Monitoring.UnitTests/ServiceControl.Monitoring.UnitTests.csproj b/src/ServiceControl.Monitoring.UnitTests/ServiceControl.Monitoring.UnitTests.csproj index 14236ddac2..05e697c0c7 100644 --- a/src/ServiceControl.Monitoring.UnitTests/ServiceControl.Monitoring.UnitTests.csproj +++ b/src/ServiceControl.Monitoring.UnitTests/ServiceControl.Monitoring.UnitTests.csproj @@ -2,6 +2,7 @@ net10.0 + Default diff --git a/src/ServiceControl.MultiInstance.AcceptanceTests/ServiceControl.MultiInstance.AcceptanceTests.csproj b/src/ServiceControl.MultiInstance.AcceptanceTests/ServiceControl.MultiInstance.AcceptanceTests.csproj index 10b78c9ba4..5998bd5906 100644 --- a/src/ServiceControl.MultiInstance.AcceptanceTests/ServiceControl.MultiInstance.AcceptanceTests.csproj +++ b/src/ServiceControl.MultiInstance.AcceptanceTests/ServiceControl.MultiInstance.AcceptanceTests.csproj @@ -2,6 +2,7 @@ net10.0 + Default diff --git a/src/ServiceControl.Persistence.Tests.InMemory/ServiceControl.Persistence.Tests.InMemory.csproj b/src/ServiceControl.Persistence.Tests.InMemory/ServiceControl.Persistence.Tests.InMemory.csproj index 5854e9fa05..bac12ff076 100644 --- a/src/ServiceControl.Persistence.Tests.InMemory/ServiceControl.Persistence.Tests.InMemory.csproj +++ b/src/ServiceControl.Persistence.Tests.InMemory/ServiceControl.Persistence.Tests.InMemory.csproj @@ -2,6 +2,7 @@ net10.0 + Default diff --git a/src/ServiceControl.Persistence.Tests.PostgreSql/ServiceControl.Persistence.Tests.PostgreSql.csproj b/src/ServiceControl.Persistence.Tests.PostgreSql/ServiceControl.Persistence.Tests.PostgreSql.csproj index 492db2ec1c..1e0bf2cebb 100644 --- a/src/ServiceControl.Persistence.Tests.PostgreSql/ServiceControl.Persistence.Tests.PostgreSql.csproj +++ b/src/ServiceControl.Persistence.Tests.PostgreSql/ServiceControl.Persistence.Tests.PostgreSql.csproj @@ -2,6 +2,7 @@ net10.0 + PostgreSqlPersistence diff --git a/src/ServiceControl.Persistence.Tests.RavenDB/ServiceControl.Persistence.Tests.RavenDB.csproj b/src/ServiceControl.Persistence.Tests.RavenDB/ServiceControl.Persistence.Tests.RavenDB.csproj index 85d3b51c65..4b8c1de262 100644 --- a/src/ServiceControl.Persistence.Tests.RavenDB/ServiceControl.Persistence.Tests.RavenDB.csproj +++ b/src/ServiceControl.Persistence.Tests.RavenDB/ServiceControl.Persistence.Tests.RavenDB.csproj @@ -2,6 +2,7 @@ net10.0 + PrimaryRavenPersistence diff --git a/src/ServiceControl.Persistence.Tests.SqlServer/ServiceControl.Persistence.Tests.SqlServer.csproj b/src/ServiceControl.Persistence.Tests.SqlServer/ServiceControl.Persistence.Tests.SqlServer.csproj index 2e762eb089..73942c6501 100644 --- a/src/ServiceControl.Persistence.Tests.SqlServer/ServiceControl.Persistence.Tests.SqlServer.csproj +++ b/src/ServiceControl.Persistence.Tests.SqlServer/ServiceControl.Persistence.Tests.SqlServer.csproj @@ -2,6 +2,7 @@ net10.0 + SqlServerPersistence diff --git a/src/ServiceControl.Transports.ASBS.Tests/ServiceControl.Transports.ASBS.Tests.csproj b/src/ServiceControl.Transports.ASBS.Tests/ServiceControl.Transports.ASBS.Tests.csproj index a7d3313ad3..fb053d09be 100644 --- a/src/ServiceControl.Transports.ASBS.Tests/ServiceControl.Transports.ASBS.Tests.csproj +++ b/src/ServiceControl.Transports.ASBS.Tests/ServiceControl.Transports.ASBS.Tests.csproj @@ -2,6 +2,7 @@ net10.0 + AzureServiceBus diff --git a/src/ServiceControl.Transports.ASQ.Tests/ServiceControl.Transports.ASQ.Tests.csproj b/src/ServiceControl.Transports.ASQ.Tests/ServiceControl.Transports.ASQ.Tests.csproj index 6a9f7fb7e9..dc1f08141d 100644 --- a/src/ServiceControl.Transports.ASQ.Tests/ServiceControl.Transports.ASQ.Tests.csproj +++ b/src/ServiceControl.Transports.ASQ.Tests/ServiceControl.Transports.ASQ.Tests.csproj @@ -1,7 +1,8 @@ - + net10.0 + AzureStorageQueues diff --git a/src/ServiceControl.Transports.IBMMQ.Tests/ServiceControl.Transports.IBMMQ.Tests.csproj b/src/ServiceControl.Transports.IBMMQ.Tests/ServiceControl.Transports.IBMMQ.Tests.csproj index f441f9cf89..c22367753e 100644 --- a/src/ServiceControl.Transports.IBMMQ.Tests/ServiceControl.Transports.IBMMQ.Tests.csproj +++ b/src/ServiceControl.Transports.IBMMQ.Tests/ServiceControl.Transports.IBMMQ.Tests.csproj @@ -2,6 +2,7 @@ net10.0 + IBMMQ diff --git a/src/ServiceControl.Transports.Msmq.Tests/ServiceControl.Transports.Msmq.Tests.csproj b/src/ServiceControl.Transports.Msmq.Tests/ServiceControl.Transports.Msmq.Tests.csproj index e94380b92c..356a3c96fc 100644 --- a/src/ServiceControl.Transports.Msmq.Tests/ServiceControl.Transports.Msmq.Tests.csproj +++ b/src/ServiceControl.Transports.Msmq.Tests/ServiceControl.Transports.Msmq.Tests.csproj @@ -2,6 +2,7 @@ net10.0-windows + MSMQ diff --git a/src/ServiceControl.Transports.PostgreSql.Tests/ServiceControl.Transports.PostgreSql.Tests.csproj b/src/ServiceControl.Transports.PostgreSql.Tests/ServiceControl.Transports.PostgreSql.Tests.csproj index 3071023ee0..2e16270353 100644 --- a/src/ServiceControl.Transports.PostgreSql.Tests/ServiceControl.Transports.PostgreSql.Tests.csproj +++ b/src/ServiceControl.Transports.PostgreSql.Tests/ServiceControl.Transports.PostgreSql.Tests.csproj @@ -2,6 +2,7 @@ net10.0 + PostgreSql diff --git a/src/ServiceControl.Transports.RabbitMQClassicConventionalRouting.Tests/ServiceControl.Transports.RabbitMQClassicConventionalRoutingTests.csproj b/src/ServiceControl.Transports.RabbitMQClassicConventionalRouting.Tests/ServiceControl.Transports.RabbitMQClassicConventionalRoutingTests.csproj index 3bd02418c1..e8bd77784a 100644 --- a/src/ServiceControl.Transports.RabbitMQClassicConventionalRouting.Tests/ServiceControl.Transports.RabbitMQClassicConventionalRoutingTests.csproj +++ b/src/ServiceControl.Transports.RabbitMQClassicConventionalRouting.Tests/ServiceControl.Transports.RabbitMQClassicConventionalRoutingTests.csproj @@ -1,7 +1,8 @@ - + net10.0 + RabbitMQ diff --git a/src/ServiceControl.Transports.RabbitMQClassicDirectRouting.Tests/ServiceControl.Transports.RabbitMQClassicDirectRouting.Tests.csproj b/src/ServiceControl.Transports.RabbitMQClassicDirectRouting.Tests/ServiceControl.Transports.RabbitMQClassicDirectRouting.Tests.csproj index d5bf028a66..e81d6b5a12 100644 --- a/src/ServiceControl.Transports.RabbitMQClassicDirectRouting.Tests/ServiceControl.Transports.RabbitMQClassicDirectRouting.Tests.csproj +++ b/src/ServiceControl.Transports.RabbitMQClassicDirectRouting.Tests/ServiceControl.Transports.RabbitMQClassicDirectRouting.Tests.csproj @@ -1,7 +1,8 @@ - + net10.0 + RabbitMQ diff --git a/src/ServiceControl.Transports.RabbitMQQuorumConventionalRouting.Tests/ServiceControl.Transports.RabbitMQQuorumConventionalRouting.Tests.csproj b/src/ServiceControl.Transports.RabbitMQQuorumConventionalRouting.Tests/ServiceControl.Transports.RabbitMQQuorumConventionalRouting.Tests.csproj index d5bf028a66..e81d6b5a12 100644 --- a/src/ServiceControl.Transports.RabbitMQQuorumConventionalRouting.Tests/ServiceControl.Transports.RabbitMQQuorumConventionalRouting.Tests.csproj +++ b/src/ServiceControl.Transports.RabbitMQQuorumConventionalRouting.Tests/ServiceControl.Transports.RabbitMQQuorumConventionalRouting.Tests.csproj @@ -1,7 +1,8 @@ - + net10.0 + RabbitMQ diff --git a/src/ServiceControl.Transports.RabbitMQQuorumDirectRouting.Tests/ServiceControl.Transports.RabbitMQQuorumDirectRouting.Tests.csproj b/src/ServiceControl.Transports.RabbitMQQuorumDirectRouting.Tests/ServiceControl.Transports.RabbitMQQuorumDirectRouting.Tests.csproj index 3bd02418c1..e8bd77784a 100644 --- a/src/ServiceControl.Transports.RabbitMQQuorumDirectRouting.Tests/ServiceControl.Transports.RabbitMQQuorumDirectRouting.Tests.csproj +++ b/src/ServiceControl.Transports.RabbitMQQuorumDirectRouting.Tests/ServiceControl.Transports.RabbitMQQuorumDirectRouting.Tests.csproj @@ -1,7 +1,8 @@ - + net10.0 + RabbitMQ diff --git a/src/ServiceControl.Transports.SQS.Tests/ServiceControl.Transports.SQS.Tests.csproj b/src/ServiceControl.Transports.SQS.Tests/ServiceControl.Transports.SQS.Tests.csproj index cee5fd3419..8057265d47 100644 --- a/src/ServiceControl.Transports.SQS.Tests/ServiceControl.Transports.SQS.Tests.csproj +++ b/src/ServiceControl.Transports.SQS.Tests/ServiceControl.Transports.SQS.Tests.csproj @@ -2,6 +2,7 @@ net10.0 + SQS diff --git a/src/ServiceControl.Transports.SqlServer.Tests/ServiceControl.Transports.SqlServer.Tests.csproj b/src/ServiceControl.Transports.SqlServer.Tests/ServiceControl.Transports.SqlServer.Tests.csproj index 62fe2403f2..a19182e7f4 100644 --- a/src/ServiceControl.Transports.SqlServer.Tests/ServiceControl.Transports.SqlServer.Tests.csproj +++ b/src/ServiceControl.Transports.SqlServer.Tests/ServiceControl.Transports.SqlServer.Tests.csproj @@ -2,6 +2,7 @@ net10.0 + SqlServer diff --git a/src/ServiceControl.Transports.Tests/ServiceControl.Transports.Tests.csproj b/src/ServiceControl.Transports.Tests/ServiceControl.Transports.Tests.csproj index 261cccf063..883bace779 100644 --- a/src/ServiceControl.Transports.Tests/ServiceControl.Transports.Tests.csproj +++ b/src/ServiceControl.Transports.Tests/ServiceControl.Transports.Tests.csproj @@ -2,6 +2,7 @@ net10.0 + Default diff --git a/src/ServiceControl.UnitTests/ServiceControl.UnitTests.csproj b/src/ServiceControl.UnitTests/ServiceControl.UnitTests.csproj index 5c3a965155..10922578ae 100644 --- a/src/ServiceControl.UnitTests/ServiceControl.UnitTests.csproj +++ b/src/ServiceControl.UnitTests/ServiceControl.UnitTests.csproj @@ -2,6 +2,7 @@ net10.0 + Default diff --git a/src/ServiceControlInstaller.Engine.UnitTests/ServiceControlInstaller.Engine.UnitTests.csproj b/src/ServiceControlInstaller.Engine.UnitTests/ServiceControlInstaller.Engine.UnitTests.csproj index 3247d3f367..77ce0c087b 100644 --- a/src/ServiceControlInstaller.Engine.UnitTests/ServiceControlInstaller.Engine.UnitTests.csproj +++ b/src/ServiceControlInstaller.Engine.UnitTests/ServiceControlInstaller.Engine.UnitTests.csproj @@ -2,6 +2,7 @@ net10.0-windows + Default diff --git a/src/ServiceControlInstaller.Packaging.UnitTests/ServiceControlInstaller.Packaging.UnitTests.csproj b/src/ServiceControlInstaller.Packaging.UnitTests/ServiceControlInstaller.Packaging.UnitTests.csproj index be95909ae0..e4ff643e53 100644 --- a/src/ServiceControlInstaller.Packaging.UnitTests/ServiceControlInstaller.Packaging.UnitTests.csproj +++ b/src/ServiceControlInstaller.Packaging.UnitTests/ServiceControlInstaller.Packaging.UnitTests.csproj @@ -2,6 +2,7 @@ net10.0 + Default diff --git a/tools/run-tests.ps1 b/tools/run-tests.ps1 new file mode 100644 index 0000000000..5927f0d073 --- /dev/null +++ b/tools/run-tests.ps1 @@ -0,0 +1,60 @@ +# Runs dotnet test against an explicit list of test projects, rather than discovering every test +# project under src. The list comes from tools/select-test-projects.ps1, so a job only pays for the +# assemblies belonging to its test category. +# +# This is a scoped replacement for Particular/run-tests-action, which has no way to be told which +# projects to run. It should fold back into that action once it grows a 'projects' input. + +param( + [Parameter(Mandatory)] + [string]$Projects, + + [string]$TargetPlatform = 'x64', + + [switch]$ReportWarnings +) + +$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" + +$reportWarningsValue = if ($ReportWarnings) { 'true' } else { 'false' } +$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 --framework $framework --logger "GitHubActions;report-warnings=$reportWarningsValue" -- RunConfiguration.TreatNoTestsAsError=true "RunConfiguration.TargetPlatform=$TargetPlatform" + + Write-Output '::endgroup::' + + if ($LASTEXITCODE -ne 0) { + Write-Output "::error::Exit code = $LASTEXITCODE" + $exitCode = 1 + } + } +} + +exit $exitCode diff --git a/tools/select-test-projects.ps1 b/tools/select-test-projects.ps1 new file mode 100644 index 0000000000..f6c50c099a --- /dev/null +++ b/tools/select-test-projects.ps1 @@ -0,0 +1,89 @@ +# Selects the test projects belonging to a test category, so that CI can build and run only those +# rather than the whole solution. The category is declared by the property in each test +# project, which must match the Filter of the assembly-level IncludeIn*Tests attribute in that project. +# +# Writes the selected project paths to $GITHUB_OUTPUT as 'test-projects', and generates an MSBuild +# traversal project so that `dotnet build` can build the whole selection in one graph. +# +# Use -List to print every category and its projects without writing any files. + +[CmdletBinding(DefaultParameterSetName = 'Select')] +param( + [Parameter(Mandatory, ParameterSetName = 'Select')] + [string]$Category, + + [Parameter(ParameterSetName = 'Select')] + [string]$TraversalProjectPath = 'tests.proj', + + [Parameter(Mandatory, ParameterSetName = 'List')] + [switch]$List +) + +$ErrorActionPreference = 'Stop' + +$projectsByCategory = @{} + +# -Filter rather than -Include: the provider applies it during traversal, which is dramatically faster +# on a tree that already contains bin/obj output. +Get-ChildItem -Path src -Filter '*.csproj' -Recurse -File | ForEach-Object { + $testCategory = (Select-Xml -Path $_.FullName -XPath "/Project/PropertyGroup/TestCategory").Node.InnerText + + if ([string]::IsNullOrWhiteSpace($testCategory)) { + return + } + + if (-not $projectsByCategory.ContainsKey($testCategory)) { + $projectsByCategory[$testCategory] = New-Object Collections.Generic.List[String] + } + + $projectsByCategory[$testCategory].Add((Resolve-Path -Relative $_.FullName)) +} + +if ($List) { + foreach ($key in $projectsByCategory.Keys | Sort-Object) { + Write-Output "$key" + $projectsByCategory[$key] | Sort-Object | ForEach-Object { Write-Output " $_" } + } + return +} + +if (-not $projectsByCategory.ContainsKey($Category)) { + $known = ($projectsByCategory.Keys | Sort-Object) -join ', ' + throw "No test project declares $Category. Known categories: $known" +} + +$projects = $projectsByCategory[$Category] | Sort-Object + +Write-Output "Test category '$Category' selects $($projects.Count) project(s):" +$projects | ForEach-Object { Write-Output " $_" } + +# ReferenceOutputAssembly is irrelevant here because the traversal project produces nothing itself. +# TargetFramework is undefined per reference so that projects targeting net10.0-windows are not forced +# onto the traversal project's own framework, matching the convention in ProjectReferences.Transports.props. +$references = $projects | ForEach-Object { + " " +} + +$traversal = @" + + + + + net10.0 + + + +$($references -join [Environment]::NewLine) + + + +"@ + +Set-Content -Path $TraversalProjectPath -Value $traversal -Encoding utf8 +Write-Output "Wrote traversal project to $TraversalProjectPath" + +if ($Env:GITHUB_OUTPUT) { + "test-projects< Date: Sat, 8 Aug 2026 14:36:46 +1000 Subject: [PATCH 2/8] Split `Default` and `RabbitMQ` test categories for CI optimization Refines the CI workflow by breaking down the broad `Default` and `RabbitMQ` test categories into more granular ones. This addresses instances where these categories were running several slow assemblies in a single job. - `Default` is split into `DefaultCore`, `DefaultAudit`, and `DefaultMonitoring`. - `RabbitMQ` is split into `RabbitMQClassicConventional`, `RabbitMQClassicDirect`, `RabbitMQQuorumConventional`, and `RabbitMQQuorumDirect`. This change enables finer-grained parallelization of tests in the CI workflow, which reduces overall execution time and improves efficiency. It also standardizes test category declarations across projects using the `` property and `IncludeInTestCategory` attribute, and updates CI conditions accordingly. --- .github/workflows/ci.yml | 16 ++++++----- README.md | 27 ++++++++++++++++--- ...icular.LicensingComponent.UnitTests.csproj | 2 +- .../TestsFilter.cs | 2 +- ...ntrol.Audit.AcceptanceTests.RavenDB.csproj | 2 +- .../TestsFilter.cs | 2 +- ...erviceControl.Audit.AcceptanceTests.csproj | 2 +- .../TestsFilter.cs | 2 +- ...rol.Audit.Persistence.Tests.RavenDB.csproj | 2 +- .../TestsFilter.cs | 2 +- ...viceControl.Audit.Persistence.Tests.csproj | 2 +- .../TestsFilter.cs | 2 +- .../ServiceControl.Audit.UnitTests.csproj | 2 +- .../TestsFilter.cs | 2 +- .../ServiceControl.Config.Tests.csproj | 2 +- .../TestsFilter.cs | 2 +- ...ServiceControl.Infrastructure.Tests.csproj | 2 +- .../TestsFilter.cs | 2 +- ...eControl.Monitoring.AcceptanceTests.csproj | 2 +- .../TestsFilter.cs | 2 +- ...ServiceControl.Monitoring.UnitTests.csproj | 2 +- .../TestsFilter.cs | 2 +- ...ntrol.MultiInstance.AcceptanceTests.csproj | 2 +- .../TestsFilter.cs | 2 +- ...eControl.Persistence.Tests.InMemory.csproj | 2 +- .../TestsFilter.cs | 2 +- ...itMQClassicConventionalRoutingTests.csproj | 2 +- .../TestsFilter.cs | 2 +- ....RabbitMQClassicDirectRouting.Tests.csproj | 2 +- .../TestsFilter.cs | 2 +- ...itMQQuorumConventionalRouting.Tests.csproj | 2 +- .../TestsFilter.cs | 2 +- ...s.RabbitMQQuorumDirectRouting.Tests.csproj | 2 +- .../TestsFilter.cs | 2 +- .../ServiceControl.Transports.Tests.csproj | 2 +- .../TestsFilter.cs | 2 +- .../ServiceControl.UnitTests.csproj | 2 +- src/ServiceControl.UnitTests/TestsFilter.cs | 2 +- ...ceControlInstaller.Engine.UnitTests.csproj | 2 +- .../TestsFilter.cs | 2 +- ...ontrolInstaller.Packaging.UnitTests.csproj | 2 +- .../TestsFilter.cs | 2 +- .../IncludeInDefaultTestsAttribute.cs | 4 --- .../IncludeInRabbitMQTestsAttribute.cs | 4 --- 44 files changed, 72 insertions(+), 59 deletions(-) delete mode 100644 src/TestHelper/IncludeInDefaultTestsAttribute.cs delete mode 100644 src/TestHelper/IncludeInRabbitMQTestsAttribute.cs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f1e3a398b8..a270209522 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,7 +18,9 @@ jobs: strategy: matrix: os: [windows-latest, ubuntu-latest] - test-category: [ Default, SqlServer, SqlServerPersistence, AzureServiceBus, RabbitMQ, AzureStorageQueues, MSMQ, SQS, PrimaryRavenAcceptance, PrimaryRavenPersistence, PostgreSql, PostgreSqlPersistence, IBMMQ ] + # Categories are declared by the property in each test project. Default and + # RabbitMQ are split because each was a single job running several slow assemblies back to back. + test-category: [ DefaultCore, DefaultAudit, DefaultMonitoring, SqlServer, SqlServerPersistence, AzureServiceBus, RabbitMQClassicConventional, RabbitMQClassicDirect, RabbitMQQuorumConventional, RabbitMQQuorumDirect, AzureStorageQueues, MSMQ, SQS, PrimaryRavenAcceptance, PrimaryRavenPersistence, PostgreSql, PostgreSqlPersistence, IBMMQ ] include: - os: windows-latest os-name: Windows @@ -46,11 +48,11 @@ jobs: - name: Select test projects id: select run: ./tools/select-test-projects.ps1 -Category ${{ matrix.test-category }} - # ServiceControlInstaller.Packaging is only in the Default category's build closure, and its zip - # targets fail without a self-contained server in deploy/. Tests never read it: RavenDB.Embedded - # supplies its own server into each test project's output directory. + # ServiceControlInstaller.Packaging is only in the DefaultCore category's build closure, and its + # zip targets fail without a self-contained server in deploy/. Tests never read it: + # RavenDB.Embedded supplies its own server into each test project's output directory. - name: Download RavenDB Server - if: matrix.test-category == 'Default' + if: matrix.test-category == 'DefaultCore' run: ./tools/download-ravendb-server.ps1 # Backgrounded so that it overlaps the infrastructure steps below. Those steps are an ordered # chain (Setup WSL provisions the Docker host the database actions rely on), so they stay in the @@ -80,7 +82,7 @@ jobs: - 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' + if: matrix.test-category == 'AzureServiceBus' || matrix.test-category == 'AzureStorageQueues' || startsWith(matrix.test-category, 'RabbitMQ') || matrix.test-category == 'PostgreSql' || matrix.test-category == 'PostgreSqlPersistence' with: creds: ${{ secrets.AZURE_ACI_CREDENTIALS }} - name: Setup SQL Server @@ -112,7 +114,7 @@ jobs: registry-password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Setup RabbitMQ uses: Particular/setup-rabbitmq-action@v1.7.1 - if: matrix.test-category == 'RabbitMQ' + if: startsWith(matrix.test-category, 'RabbitMQ') with: connection-string-name: ServiceControl_TransportTests_RabbitMQ_ConnectionString tag: ServiceControl diff --git a/README.md b/README.md index bdff9eb042..c0bde0c286 100644 --- a/README.md +++ b/README.md @@ -46,18 +46,37 @@ Testing using the [CI workflow](/.github/workflows/ci.yml) depends on the follow Running all tests all the times takes a lot of resources. Tests are filtered based on the `ServiceControl_TESTS_FILTER` environment variable. To run only a subset, e.g., SQS transport tests, define the variable as `ServiceControl_TESTS_FILTER=SQS`. The following list contains all the possible `ServiceControl_TESTS_FILTER` values: -- `Default` - runs only non-transport-specific tests +Non-transport-specific: + +- `DefaultCore` +- `DefaultAudit` +- `DefaultMonitoring` + +Transports: + - `AzureServiceBus` - `AzureStorageQueues` +- `IBMMQ` - `MSMQ` -- `RabbitMQ` +- `PostgreSql` +- `RabbitMQClassicConventional` +- `RabbitMQClassicDirect` +- `RabbitMQQuorumConventional` +- `RabbitMQQuorumDirect` - `SqlServer` -- `SqlServerPersistence` -- `PostgresSqlPersistence` - `SQS` +Persisters: + +- `PostgreSqlPersistence` +- `PrimaryRavenAcceptance` +- `PrimaryRavenPersistence` +- `SqlServerPersistence` + NOTE: If no variable is defined all tests will be executed. +Each category is declared by the `` property in the test project and by the matching assembly-level `IncludeInTestCategory` attribute. CI uses the property to build and run only that category's projects; the attribute is the runtime safety net. Run `./tools/select-test-projects.ps1 -List` to see every category and the projects it selects. + ## Security Configuration Documentation for configuring security features: diff --git a/src/Particular.LicensingComponent.UnitTests/Particular.LicensingComponent.UnitTests.csproj b/src/Particular.LicensingComponent.UnitTests/Particular.LicensingComponent.UnitTests.csproj index 4c62b01f22..529a3f226c 100644 --- a/src/Particular.LicensingComponent.UnitTests/Particular.LicensingComponent.UnitTests.csproj +++ b/src/Particular.LicensingComponent.UnitTests/Particular.LicensingComponent.UnitTests.csproj @@ -2,7 +2,7 @@ net10.0 - Default + DefaultCore diff --git a/src/Particular.LicensingComponent.UnitTests/TestsFilter.cs b/src/Particular.LicensingComponent.UnitTests/TestsFilter.cs index cbb64ab1fc..aab6eb2319 100644 --- a/src/Particular.LicensingComponent.UnitTests/TestsFilter.cs +++ b/src/Particular.LicensingComponent.UnitTests/TestsFilter.cs @@ -1 +1 @@ -[assembly: IncludeInDefaultTests()] \ No newline at end of file +[assembly: IncludeInTestCategory("DefaultCore")] \ No newline at end of file diff --git a/src/ServiceControl.Audit.AcceptanceTests.RavenDB/ServiceControl.Audit.AcceptanceTests.RavenDB.csproj b/src/ServiceControl.Audit.AcceptanceTests.RavenDB/ServiceControl.Audit.AcceptanceTests.RavenDB.csproj index b6da0faf58..f3834a4a81 100644 --- a/src/ServiceControl.Audit.AcceptanceTests.RavenDB/ServiceControl.Audit.AcceptanceTests.RavenDB.csproj +++ b/src/ServiceControl.Audit.AcceptanceTests.RavenDB/ServiceControl.Audit.AcceptanceTests.RavenDB.csproj @@ -2,7 +2,7 @@ net10.0 - Default + DefaultAudit diff --git a/src/ServiceControl.Audit.AcceptanceTests.RavenDB/TestsFilter.cs b/src/ServiceControl.Audit.AcceptanceTests.RavenDB/TestsFilter.cs index cbb64ab1fc..653413cf8e 100644 --- a/src/ServiceControl.Audit.AcceptanceTests.RavenDB/TestsFilter.cs +++ b/src/ServiceControl.Audit.AcceptanceTests.RavenDB/TestsFilter.cs @@ -1 +1 @@ -[assembly: IncludeInDefaultTests()] \ No newline at end of file +[assembly: IncludeInTestCategory("DefaultAudit")] \ No newline at end of file diff --git a/src/ServiceControl.Audit.AcceptanceTests/ServiceControl.Audit.AcceptanceTests.csproj b/src/ServiceControl.Audit.AcceptanceTests/ServiceControl.Audit.AcceptanceTests.csproj index 0d59ffe0ef..2388115142 100644 --- a/src/ServiceControl.Audit.AcceptanceTests/ServiceControl.Audit.AcceptanceTests.csproj +++ b/src/ServiceControl.Audit.AcceptanceTests/ServiceControl.Audit.AcceptanceTests.csproj @@ -2,7 +2,7 @@ net10.0 - Default + DefaultAudit diff --git a/src/ServiceControl.Audit.AcceptanceTests/TestsFilter.cs b/src/ServiceControl.Audit.AcceptanceTests/TestsFilter.cs index cbb64ab1fc..653413cf8e 100644 --- a/src/ServiceControl.Audit.AcceptanceTests/TestsFilter.cs +++ b/src/ServiceControl.Audit.AcceptanceTests/TestsFilter.cs @@ -1 +1 @@ -[assembly: IncludeInDefaultTests()] \ No newline at end of file +[assembly: IncludeInTestCategory("DefaultAudit")] \ No newline at end of file diff --git a/src/ServiceControl.Audit.Persistence.Tests.RavenDB/ServiceControl.Audit.Persistence.Tests.RavenDB.csproj b/src/ServiceControl.Audit.Persistence.Tests.RavenDB/ServiceControl.Audit.Persistence.Tests.RavenDB.csproj index 1d1e5d2635..3dd94be5ea 100644 --- a/src/ServiceControl.Audit.Persistence.Tests.RavenDB/ServiceControl.Audit.Persistence.Tests.RavenDB.csproj +++ b/src/ServiceControl.Audit.Persistence.Tests.RavenDB/ServiceControl.Audit.Persistence.Tests.RavenDB.csproj @@ -2,7 +2,7 @@ net10.0 - Default + DefaultAudit diff --git a/src/ServiceControl.Audit.Persistence.Tests.RavenDB/TestsFilter.cs b/src/ServiceControl.Audit.Persistence.Tests.RavenDB/TestsFilter.cs index cbb64ab1fc..653413cf8e 100644 --- a/src/ServiceControl.Audit.Persistence.Tests.RavenDB/TestsFilter.cs +++ b/src/ServiceControl.Audit.Persistence.Tests.RavenDB/TestsFilter.cs @@ -1 +1 @@ -[assembly: IncludeInDefaultTests()] \ No newline at end of file +[assembly: IncludeInTestCategory("DefaultAudit")] \ No newline at end of file diff --git a/src/ServiceControl.Audit.Persistence.Tests/ServiceControl.Audit.Persistence.Tests.csproj b/src/ServiceControl.Audit.Persistence.Tests/ServiceControl.Audit.Persistence.Tests.csproj index 8d02310f98..ece853badc 100644 --- a/src/ServiceControl.Audit.Persistence.Tests/ServiceControl.Audit.Persistence.Tests.csproj +++ b/src/ServiceControl.Audit.Persistence.Tests/ServiceControl.Audit.Persistence.Tests.csproj @@ -2,7 +2,7 @@ net10.0 - Default + DefaultAudit diff --git a/src/ServiceControl.Audit.Persistence.Tests/TestsFilter.cs b/src/ServiceControl.Audit.Persistence.Tests/TestsFilter.cs index cbb64ab1fc..653413cf8e 100644 --- a/src/ServiceControl.Audit.Persistence.Tests/TestsFilter.cs +++ b/src/ServiceControl.Audit.Persistence.Tests/TestsFilter.cs @@ -1 +1 @@ -[assembly: IncludeInDefaultTests()] \ No newline at end of file +[assembly: IncludeInTestCategory("DefaultAudit")] \ No newline at end of file diff --git a/src/ServiceControl.Audit.UnitTests/ServiceControl.Audit.UnitTests.csproj b/src/ServiceControl.Audit.UnitTests/ServiceControl.Audit.UnitTests.csproj index 0b26c9f9a0..44a4d9efa0 100644 --- a/src/ServiceControl.Audit.UnitTests/ServiceControl.Audit.UnitTests.csproj +++ b/src/ServiceControl.Audit.UnitTests/ServiceControl.Audit.UnitTests.csproj @@ -2,7 +2,7 @@ net10.0 - Default + DefaultAudit diff --git a/src/ServiceControl.Audit.UnitTests/TestsFilter.cs b/src/ServiceControl.Audit.UnitTests/TestsFilter.cs index cbb64ab1fc..653413cf8e 100644 --- a/src/ServiceControl.Audit.UnitTests/TestsFilter.cs +++ b/src/ServiceControl.Audit.UnitTests/TestsFilter.cs @@ -1 +1 @@ -[assembly: IncludeInDefaultTests()] \ No newline at end of file +[assembly: IncludeInTestCategory("DefaultAudit")] \ No newline at end of file diff --git a/src/ServiceControl.Config.Tests/ServiceControl.Config.Tests.csproj b/src/ServiceControl.Config.Tests/ServiceControl.Config.Tests.csproj index 17ca280f67..6c0a7c644b 100644 --- a/src/ServiceControl.Config.Tests/ServiceControl.Config.Tests.csproj +++ b/src/ServiceControl.Config.Tests/ServiceControl.Config.Tests.csproj @@ -2,7 +2,7 @@ net10.0-windows10.0.19041.0 - Default + DefaultCore x64 true diff --git a/src/ServiceControl.Config.Tests/TestsFilter.cs b/src/ServiceControl.Config.Tests/TestsFilter.cs index cbb64ab1fc..aab6eb2319 100644 --- a/src/ServiceControl.Config.Tests/TestsFilter.cs +++ b/src/ServiceControl.Config.Tests/TestsFilter.cs @@ -1 +1 @@ -[assembly: IncludeInDefaultTests()] \ No newline at end of file +[assembly: IncludeInTestCategory("DefaultCore")] \ No newline at end of file diff --git a/src/ServiceControl.Infrastructure.Tests/ServiceControl.Infrastructure.Tests.csproj b/src/ServiceControl.Infrastructure.Tests/ServiceControl.Infrastructure.Tests.csproj index 8feaa7df5a..5c53d4918f 100644 --- a/src/ServiceControl.Infrastructure.Tests/ServiceControl.Infrastructure.Tests.csproj +++ b/src/ServiceControl.Infrastructure.Tests/ServiceControl.Infrastructure.Tests.csproj @@ -2,7 +2,7 @@ net10.0 - Default + DefaultCore diff --git a/src/ServiceControl.Infrastructure.Tests/TestsFilter.cs b/src/ServiceControl.Infrastructure.Tests/TestsFilter.cs index cbb64ab1fc..aab6eb2319 100644 --- a/src/ServiceControl.Infrastructure.Tests/TestsFilter.cs +++ b/src/ServiceControl.Infrastructure.Tests/TestsFilter.cs @@ -1 +1 @@ -[assembly: IncludeInDefaultTests()] \ No newline at end of file +[assembly: IncludeInTestCategory("DefaultCore")] \ No newline at end of file diff --git a/src/ServiceControl.Monitoring.AcceptanceTests/ServiceControl.Monitoring.AcceptanceTests.csproj b/src/ServiceControl.Monitoring.AcceptanceTests/ServiceControl.Monitoring.AcceptanceTests.csproj index 54b53b33e5..dd95e8cfad 100644 --- a/src/ServiceControl.Monitoring.AcceptanceTests/ServiceControl.Monitoring.AcceptanceTests.csproj +++ b/src/ServiceControl.Monitoring.AcceptanceTests/ServiceControl.Monitoring.AcceptanceTests.csproj @@ -2,7 +2,7 @@ net10.0 - Default + DefaultMonitoring diff --git a/src/ServiceControl.Monitoring.AcceptanceTests/TestsFilter.cs b/src/ServiceControl.Monitoring.AcceptanceTests/TestsFilter.cs index cbb64ab1fc..35e9af11b9 100644 --- a/src/ServiceControl.Monitoring.AcceptanceTests/TestsFilter.cs +++ b/src/ServiceControl.Monitoring.AcceptanceTests/TestsFilter.cs @@ -1 +1 @@ -[assembly: IncludeInDefaultTests()] \ No newline at end of file +[assembly: IncludeInTestCategory("DefaultMonitoring")] \ No newline at end of file diff --git a/src/ServiceControl.Monitoring.UnitTests/ServiceControl.Monitoring.UnitTests.csproj b/src/ServiceControl.Monitoring.UnitTests/ServiceControl.Monitoring.UnitTests.csproj index 05e697c0c7..9261529433 100644 --- a/src/ServiceControl.Monitoring.UnitTests/ServiceControl.Monitoring.UnitTests.csproj +++ b/src/ServiceControl.Monitoring.UnitTests/ServiceControl.Monitoring.UnitTests.csproj @@ -2,7 +2,7 @@ net10.0 - Default + DefaultMonitoring diff --git a/src/ServiceControl.Monitoring.UnitTests/TestsFilter.cs b/src/ServiceControl.Monitoring.UnitTests/TestsFilter.cs index cbb64ab1fc..35e9af11b9 100644 --- a/src/ServiceControl.Monitoring.UnitTests/TestsFilter.cs +++ b/src/ServiceControl.Monitoring.UnitTests/TestsFilter.cs @@ -1 +1 @@ -[assembly: IncludeInDefaultTests()] \ No newline at end of file +[assembly: IncludeInTestCategory("DefaultMonitoring")] \ No newline at end of file diff --git a/src/ServiceControl.MultiInstance.AcceptanceTests/ServiceControl.MultiInstance.AcceptanceTests.csproj b/src/ServiceControl.MultiInstance.AcceptanceTests/ServiceControl.MultiInstance.AcceptanceTests.csproj index 5998bd5906..cbaba829ae 100644 --- a/src/ServiceControl.MultiInstance.AcceptanceTests/ServiceControl.MultiInstance.AcceptanceTests.csproj +++ b/src/ServiceControl.MultiInstance.AcceptanceTests/ServiceControl.MultiInstance.AcceptanceTests.csproj @@ -2,7 +2,7 @@ net10.0 - Default + DefaultCore diff --git a/src/ServiceControl.MultiInstance.AcceptanceTests/TestsFilter.cs b/src/ServiceControl.MultiInstance.AcceptanceTests/TestsFilter.cs index cbb64ab1fc..aab6eb2319 100644 --- a/src/ServiceControl.MultiInstance.AcceptanceTests/TestsFilter.cs +++ b/src/ServiceControl.MultiInstance.AcceptanceTests/TestsFilter.cs @@ -1 +1 @@ -[assembly: IncludeInDefaultTests()] \ No newline at end of file +[assembly: IncludeInTestCategory("DefaultCore")] \ No newline at end of file diff --git a/src/ServiceControl.Persistence.Tests.InMemory/ServiceControl.Persistence.Tests.InMemory.csproj b/src/ServiceControl.Persistence.Tests.InMemory/ServiceControl.Persistence.Tests.InMemory.csproj index bac12ff076..7bf877fa51 100644 --- a/src/ServiceControl.Persistence.Tests.InMemory/ServiceControl.Persistence.Tests.InMemory.csproj +++ b/src/ServiceControl.Persistence.Tests.InMemory/ServiceControl.Persistence.Tests.InMemory.csproj @@ -2,7 +2,7 @@ net10.0 - Default + DefaultCore diff --git a/src/ServiceControl.Persistence.Tests.InMemory/TestsFilter.cs b/src/ServiceControl.Persistence.Tests.InMemory/TestsFilter.cs index cbb64ab1fc..aab6eb2319 100644 --- a/src/ServiceControl.Persistence.Tests.InMemory/TestsFilter.cs +++ b/src/ServiceControl.Persistence.Tests.InMemory/TestsFilter.cs @@ -1 +1 @@ -[assembly: IncludeInDefaultTests()] \ No newline at end of file +[assembly: IncludeInTestCategory("DefaultCore")] \ No newline at end of file diff --git a/src/ServiceControl.Transports.RabbitMQClassicConventionalRouting.Tests/ServiceControl.Transports.RabbitMQClassicConventionalRoutingTests.csproj b/src/ServiceControl.Transports.RabbitMQClassicConventionalRouting.Tests/ServiceControl.Transports.RabbitMQClassicConventionalRoutingTests.csproj index e8bd77784a..ae91141a0e 100644 --- a/src/ServiceControl.Transports.RabbitMQClassicConventionalRouting.Tests/ServiceControl.Transports.RabbitMQClassicConventionalRoutingTests.csproj +++ b/src/ServiceControl.Transports.RabbitMQClassicConventionalRouting.Tests/ServiceControl.Transports.RabbitMQClassicConventionalRoutingTests.csproj @@ -2,7 +2,7 @@ net10.0 - RabbitMQ + RabbitMQClassicConventional diff --git a/src/ServiceControl.Transports.RabbitMQClassicConventionalRouting.Tests/TestsFilter.cs b/src/ServiceControl.Transports.RabbitMQClassicConventionalRouting.Tests/TestsFilter.cs index 3d1ec7f59c..b8b0d9f08b 100644 --- a/src/ServiceControl.Transports.RabbitMQClassicConventionalRouting.Tests/TestsFilter.cs +++ b/src/ServiceControl.Transports.RabbitMQClassicConventionalRouting.Tests/TestsFilter.cs @@ -1 +1 @@ -[assembly: IncludeInRabbitMQTests()] \ No newline at end of file +[assembly: IncludeInTestCategory("RabbitMQClassicConventional")] \ No newline at end of file diff --git a/src/ServiceControl.Transports.RabbitMQClassicDirectRouting.Tests/ServiceControl.Transports.RabbitMQClassicDirectRouting.Tests.csproj b/src/ServiceControl.Transports.RabbitMQClassicDirectRouting.Tests/ServiceControl.Transports.RabbitMQClassicDirectRouting.Tests.csproj index e81d6b5a12..c8b65116c8 100644 --- a/src/ServiceControl.Transports.RabbitMQClassicDirectRouting.Tests/ServiceControl.Transports.RabbitMQClassicDirectRouting.Tests.csproj +++ b/src/ServiceControl.Transports.RabbitMQClassicDirectRouting.Tests/ServiceControl.Transports.RabbitMQClassicDirectRouting.Tests.csproj @@ -2,7 +2,7 @@ net10.0 - RabbitMQ + RabbitMQClassicDirect diff --git a/src/ServiceControl.Transports.RabbitMQClassicDirectRouting.Tests/TestsFilter.cs b/src/ServiceControl.Transports.RabbitMQClassicDirectRouting.Tests/TestsFilter.cs index 3d1ec7f59c..605a7b298b 100644 --- a/src/ServiceControl.Transports.RabbitMQClassicDirectRouting.Tests/TestsFilter.cs +++ b/src/ServiceControl.Transports.RabbitMQClassicDirectRouting.Tests/TestsFilter.cs @@ -1 +1 @@ -[assembly: IncludeInRabbitMQTests()] \ No newline at end of file +[assembly: IncludeInTestCategory("RabbitMQClassicDirect")] \ No newline at end of file diff --git a/src/ServiceControl.Transports.RabbitMQQuorumConventionalRouting.Tests/ServiceControl.Transports.RabbitMQQuorumConventionalRouting.Tests.csproj b/src/ServiceControl.Transports.RabbitMQQuorumConventionalRouting.Tests/ServiceControl.Transports.RabbitMQQuorumConventionalRouting.Tests.csproj index e81d6b5a12..0e5b9c9b3a 100644 --- a/src/ServiceControl.Transports.RabbitMQQuorumConventionalRouting.Tests/ServiceControl.Transports.RabbitMQQuorumConventionalRouting.Tests.csproj +++ b/src/ServiceControl.Transports.RabbitMQQuorumConventionalRouting.Tests/ServiceControl.Transports.RabbitMQQuorumConventionalRouting.Tests.csproj @@ -2,7 +2,7 @@ net10.0 - RabbitMQ + RabbitMQQuorumConventional diff --git a/src/ServiceControl.Transports.RabbitMQQuorumConventionalRouting.Tests/TestsFilter.cs b/src/ServiceControl.Transports.RabbitMQQuorumConventionalRouting.Tests/TestsFilter.cs index 3d1ec7f59c..4587c56b95 100644 --- a/src/ServiceControl.Transports.RabbitMQQuorumConventionalRouting.Tests/TestsFilter.cs +++ b/src/ServiceControl.Transports.RabbitMQQuorumConventionalRouting.Tests/TestsFilter.cs @@ -1 +1 @@ -[assembly: IncludeInRabbitMQTests()] \ No newline at end of file +[assembly: IncludeInTestCategory("RabbitMQQuorumConventional")] \ No newline at end of file diff --git a/src/ServiceControl.Transports.RabbitMQQuorumDirectRouting.Tests/ServiceControl.Transports.RabbitMQQuorumDirectRouting.Tests.csproj b/src/ServiceControl.Transports.RabbitMQQuorumDirectRouting.Tests/ServiceControl.Transports.RabbitMQQuorumDirectRouting.Tests.csproj index e8bd77784a..c9cf087156 100644 --- a/src/ServiceControl.Transports.RabbitMQQuorumDirectRouting.Tests/ServiceControl.Transports.RabbitMQQuorumDirectRouting.Tests.csproj +++ b/src/ServiceControl.Transports.RabbitMQQuorumDirectRouting.Tests/ServiceControl.Transports.RabbitMQQuorumDirectRouting.Tests.csproj @@ -2,7 +2,7 @@ net10.0 - RabbitMQ + RabbitMQQuorumDirect diff --git a/src/ServiceControl.Transports.RabbitMQQuorumDirectRouting.Tests/TestsFilter.cs b/src/ServiceControl.Transports.RabbitMQQuorumDirectRouting.Tests/TestsFilter.cs index 3d1ec7f59c..9977bd7ab6 100644 --- a/src/ServiceControl.Transports.RabbitMQQuorumDirectRouting.Tests/TestsFilter.cs +++ b/src/ServiceControl.Transports.RabbitMQQuorumDirectRouting.Tests/TestsFilter.cs @@ -1 +1 @@ -[assembly: IncludeInRabbitMQTests()] \ No newline at end of file +[assembly: IncludeInTestCategory("RabbitMQQuorumDirect")] \ No newline at end of file diff --git a/src/ServiceControl.Transports.Tests/ServiceControl.Transports.Tests.csproj b/src/ServiceControl.Transports.Tests/ServiceControl.Transports.Tests.csproj index 883bace779..fd649e1b60 100644 --- a/src/ServiceControl.Transports.Tests/ServiceControl.Transports.Tests.csproj +++ b/src/ServiceControl.Transports.Tests/ServiceControl.Transports.Tests.csproj @@ -2,7 +2,7 @@ net10.0 - Default + DefaultCore diff --git a/src/ServiceControl.Transports.Tests/TestsFilter.cs b/src/ServiceControl.Transports.Tests/TestsFilter.cs index cbb64ab1fc..aab6eb2319 100644 --- a/src/ServiceControl.Transports.Tests/TestsFilter.cs +++ b/src/ServiceControl.Transports.Tests/TestsFilter.cs @@ -1 +1 @@ -[assembly: IncludeInDefaultTests()] \ No newline at end of file +[assembly: IncludeInTestCategory("DefaultCore")] \ No newline at end of file diff --git a/src/ServiceControl.UnitTests/ServiceControl.UnitTests.csproj b/src/ServiceControl.UnitTests/ServiceControl.UnitTests.csproj index 10922578ae..280ef9eced 100644 --- a/src/ServiceControl.UnitTests/ServiceControl.UnitTests.csproj +++ b/src/ServiceControl.UnitTests/ServiceControl.UnitTests.csproj @@ -2,7 +2,7 @@ net10.0 - Default + DefaultCore diff --git a/src/ServiceControl.UnitTests/TestsFilter.cs b/src/ServiceControl.UnitTests/TestsFilter.cs index cbb64ab1fc..aab6eb2319 100644 --- a/src/ServiceControl.UnitTests/TestsFilter.cs +++ b/src/ServiceControl.UnitTests/TestsFilter.cs @@ -1 +1 @@ -[assembly: IncludeInDefaultTests()] \ No newline at end of file +[assembly: IncludeInTestCategory("DefaultCore")] \ No newline at end of file diff --git a/src/ServiceControlInstaller.Engine.UnitTests/ServiceControlInstaller.Engine.UnitTests.csproj b/src/ServiceControlInstaller.Engine.UnitTests/ServiceControlInstaller.Engine.UnitTests.csproj index 77ce0c087b..3908dc1ba1 100644 --- a/src/ServiceControlInstaller.Engine.UnitTests/ServiceControlInstaller.Engine.UnitTests.csproj +++ b/src/ServiceControlInstaller.Engine.UnitTests/ServiceControlInstaller.Engine.UnitTests.csproj @@ -2,7 +2,7 @@ net10.0-windows - Default + DefaultCore diff --git a/src/ServiceControlInstaller.Engine.UnitTests/TestsFilter.cs b/src/ServiceControlInstaller.Engine.UnitTests/TestsFilter.cs index cbb64ab1fc..aab6eb2319 100644 --- a/src/ServiceControlInstaller.Engine.UnitTests/TestsFilter.cs +++ b/src/ServiceControlInstaller.Engine.UnitTests/TestsFilter.cs @@ -1 +1 @@ -[assembly: IncludeInDefaultTests()] \ No newline at end of file +[assembly: IncludeInTestCategory("DefaultCore")] \ No newline at end of file diff --git a/src/ServiceControlInstaller.Packaging.UnitTests/ServiceControlInstaller.Packaging.UnitTests.csproj b/src/ServiceControlInstaller.Packaging.UnitTests/ServiceControlInstaller.Packaging.UnitTests.csproj index e4ff643e53..38dd4fbc7d 100644 --- a/src/ServiceControlInstaller.Packaging.UnitTests/ServiceControlInstaller.Packaging.UnitTests.csproj +++ b/src/ServiceControlInstaller.Packaging.UnitTests/ServiceControlInstaller.Packaging.UnitTests.csproj @@ -2,7 +2,7 @@ net10.0 - Default + DefaultCore diff --git a/src/ServiceControlInstaller.Packaging.UnitTests/TestsFilter.cs b/src/ServiceControlInstaller.Packaging.UnitTests/TestsFilter.cs index cbb64ab1fc..aab6eb2319 100644 --- a/src/ServiceControlInstaller.Packaging.UnitTests/TestsFilter.cs +++ b/src/ServiceControlInstaller.Packaging.UnitTests/TestsFilter.cs @@ -1 +1 @@ -[assembly: IncludeInDefaultTests()] \ No newline at end of file +[assembly: IncludeInTestCategory("DefaultCore")] \ No newline at end of file diff --git a/src/TestHelper/IncludeInDefaultTestsAttribute.cs b/src/TestHelper/IncludeInDefaultTestsAttribute.cs deleted file mode 100644 index 83442d9b40..0000000000 --- a/src/TestHelper/IncludeInDefaultTestsAttribute.cs +++ /dev/null @@ -1,4 +0,0 @@ -public class IncludeInDefaultTestsAttribute : IncludeInTestsAttribute -{ - protected override string Filter => "Default"; -} \ No newline at end of file diff --git a/src/TestHelper/IncludeInRabbitMQTestsAttribute.cs b/src/TestHelper/IncludeInRabbitMQTestsAttribute.cs deleted file mode 100644 index 23002f4336..0000000000 --- a/src/TestHelper/IncludeInRabbitMQTestsAttribute.cs +++ /dev/null @@ -1,4 +0,0 @@ -public class IncludeInRabbitMQTestsAttribute : IncludeInTestsAttribute -{ - protected override string Filter => "RabbitMQ"; -} From dec7c7147d2565bec49472f8a63731db869c1430 Mon Sep 17 00:00:00 2001 From: John Simons Date: Sat, 8 Aug 2026 15:11:56 +1000 Subject: [PATCH 3/8] Introduce local actions for containerized RabbitMQ and IBM MQ in CI This change introduces new composite GitHub Actions to set up RabbitMQ and IBM MQ in containers. It enables IBM MQ to run on Windows runners by utilizing Docker within WSL2. The RabbitMQ setup is also updated to use a local container action, replacing the external `Particular/setup-rabbitmq-action` and ensuring consistent container-based provisioning across Linux and Windows via WSL2. As a result, these services no longer require Azure authentication in the CI workflow. --- .github/actions/setup-ibmmq/action.yml | 80 ++++++++++++++++ .github/actions/setup-rabbitmq/action.yml | 110 ++++++++++++++++++++++ .github/workflows/ci.yml | 26 +++-- 3 files changed, 201 insertions(+), 15 deletions(-) create mode 100644 .github/actions/setup-ibmmq/action.yml create mode 100644 .github/actions/setup-rabbitmq/action.yml diff --git a/.github/actions/setup-ibmmq/action.yml b/.github/actions/setup-ibmmq/action.yml new file mode 100644 index 0000000000..58bb5c11a6 --- /dev/null +++ b/.github/actions/setup-ibmmq/action.yml @@ -0,0 +1,80 @@ +name: Setup IBM MQ +description: > + Runs IBM MQ in a Linux container for the duration of the job. On Linux runners the container runs + directly through Docker. On Windows runners it runs inside the WSL2 Docker host provisioned by + Particular/setup-wsl-action, which must run first, because the IBM MQ image is Linux-only and Windows + runners can otherwise only run Windows containers. + +inputs: + connection-string-name: + description: The name of the environment variable to fill with the IBM MQ connection string. + required: true + queue-manager: + description: The name of the queue manager to create. + required: false + default: QM1 + admin-password: + description: The password for the admin user. + required: false + default: passw0rd + image: + description: The IBM MQ container image to run. + required: false + default: icr.io/ibm-messaging/mq:latest + +runs: + using: composite + steps: + - name: Run IBM MQ container + shell: pwsh + env: + CONNECTION_STRING_NAME: ${{ inputs.connection-string-name }} + QUEUE_MANAGER: ${{ inputs.queue-manager }} + ADMIN_PASSWORD: ${{ inputs.admin-password }} + IMAGE: ${{ inputs.image }} + run: | + $ErrorActionPreference = 'Stop' + + $onWindows = $Env:RUNNER_OS -eq 'Windows' + + if ($onWindows) { + if (-not $Env:WSL_TOOLS_MODULE_PATH) { + throw "WSL_TOOLS_MODULE_PATH is not set. Particular/setup-wsl-action must run before this action on Windows." + } + Import-Module $Env:WSL_TOOLS_MODULE_PATH + } + + function Invoke-Docker([string]$Arguments) { + if ($onWindows) { Invoke-Wsl -CheckExitCode -Command "docker $Arguments" } + else { Invoke-Expression "docker $Arguments" } + } + + $containerName = 'ibmmq' + + Write-Output "Starting $($Env:IMAGE) as $containerName" + Invoke-Docker ("run --name $containerName --detach --publish 1414:1414 --publish 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=$($Env:QUEUE_MANAGER) -e MQ_ADMIN_PASSWORD=$($Env:ADMIN_PASSWORD) " + + $Env:IMAGE) + + Write-Output "::group::Waiting for container health check" + + # Out-String because the WSL and native paths differ in whether they hand back a string or a + # collection of output lines. + $deadline = (Get-Date).AddMinutes(5) + while ((Invoke-Docker "inspect --format '{{.State.Health.Status}}' $containerName" | Out-String).Trim() -ne 'healthy') { + if ((Get-Date) -gt $deadline) { + Invoke-Docker "logs $containerName" + throw "IBM MQ did not report healthy within 5 minutes." + } + Start-Sleep -Seconds 2 + } + + Write-Output "::endgroup::" + + # On Windows the queue manager listens inside the WSL VM, reachable on its gateway address. + $mqHost = if ($onWindows) { $Env:WSL_IP } else { 'localhost' } + $connectionString = "mq://admin:$($Env:ADMIN_PASSWORD)@${mqHost}:1414/$($Env:QUEUE_MANAGER)?channel=DEV.ADMIN.SVRCONN&topicprefix=DEV" + + Write-Output "IBM MQ is healthy on $mqHost" + Write-Output "$($Env:CONNECTION_STRING_NAME)=$connectionString" | Out-File -FilePath $Env:GITHUB_ENV -Encoding utf-8 -Append diff --git a/.github/actions/setup-rabbitmq/action.yml b/.github/actions/setup-rabbitmq/action.yml new file mode 100644 index 0000000000..05f6ab0e39 --- /dev/null +++ b/.github/actions/setup-rabbitmq/action.yml @@ -0,0 +1,110 @@ +name: Setup RabbitMQ +description: > + Runs RabbitMQ in a Linux container for the duration of the job. On Linux runners the container runs + directly through Docker. On Windows runners it runs inside the WSL2 Docker host provisioned by + Particular/setup-wsl-action, which must run first. + + This is a local stand-in for Particular/setup-rabbitmq-action, which provisions an Azure Container + Instance on Windows. It produces the same connection string, so it can be swapped back out once the + shared action gains WSL support. + +inputs: + connection-string-name: + description: The name of the environment variable to fill with the RabbitMQ connection string. + required: true + host-env-var-name: + description: The name of an environment variable to fill with the RabbitMQ host name. + required: false + image-tag: + description: The tag of the rabbitmq container image. + required: false + default: '3-management' + registry-login-server: + description: The container registry to log in to, if required. + required: false + default: index.docker.io + registry-username: + description: The username for the container registry. No login is attempted if not provided. + required: false + registry-password: + description: The password for the container registry. No login is attempted if not provided. + required: false + +runs: + using: composite + steps: + - name: Run RabbitMQ container + shell: pwsh + env: + CONNECTION_STRING_NAME: ${{ inputs.connection-string-name }} + HOST_ENV_VAR_NAME: ${{ inputs.host-env-var-name }} + IMAGE_TAG: ${{ inputs.image-tag }} + REGISTRY_LOGIN_SERVER: ${{ inputs.registry-login-server }} + REGISTRY_USERNAME: ${{ inputs.registry-username }} + REGISTRY_PASSWORD: ${{ inputs.registry-password }} + run: | + $ErrorActionPreference = 'Stop' + + $onWindows = $Env:RUNNER_OS -eq 'Windows' + + if ($onWindows) { + if (-not $Env:WSL_TOOLS_MODULE_PATH) { + throw "WSL_TOOLS_MODULE_PATH is not set. Particular/setup-wsl-action must run before this action on Windows." + } + Import-Module $Env:WSL_TOOLS_MODULE_PATH + } + + function Invoke-Docker([string]$Arguments) { + if ($onWindows) { Invoke-Wsl -CheckExitCode -Command "docker $Arguments" } + else { Invoke-Expression "docker $Arguments" } + } + + $containerName = 'servicecontrol-rabbitmq' + $image = "rabbitmq:$($Env:IMAGE_TAG)" + + if ($Env:REGISTRY_USERNAME -and $Env:REGISTRY_PASSWORD) { + Write-Output "Logging in to $($Env:REGISTRY_LOGIN_SERVER)" + Invoke-Docker "login $($Env:REGISTRY_LOGIN_SERVER) --username $($Env:REGISTRY_USERNAME) --password $($Env:REGISTRY_PASSWORD)" | Out-Null + } + else { + Write-Output "No registry credentials supplied, pulling anonymously" + } + + Write-Output "Starting $image as $containerName" + Invoke-Docker "run --name $containerName --detach --publish 5672:5672 --publish 15672:15672 $image" + + # On Windows the broker listens inside the WSL VM, reachable on its gateway address. + $ipAddress = if ($onWindows) { $Env:WSL_IP } else { '127.0.0.1' } + + Write-Output "$($Env:CONNECTION_STRING_NAME)=host=$ipAddress" | Out-File -FilePath $Env:GITHUB_ENV -Encoding utf-8 -Append + if (-not [string]::IsNullOrWhiteSpace($Env:HOST_ENV_VAR_NAME)) { + Write-Output "$($Env:HOST_ENV_VAR_NAME)=$ipAddress" | Out-File -FilePath $Env:GITHUB_ENV -Encoding utf-8 -Append + } + + Write-Output "::group::Testing connection" + + # Explicit header rather than -Credential, which refuses basic auth over plain HTTP in pwsh. + $authHeader = @{ Authorization = 'Basic ' + [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes('guest:guest')) } + $healthUri = "http://${ipAddress}:15672/api/health/checks/virtual-hosts" + $deadline = (Get-Date).AddMinutes(5) + + while ($true) { + try { + $response = Invoke-RestMethod -Uri $healthUri -Headers $authHeader -TimeoutSec 5 + if ($response.status -eq 'ok') { break } + Write-Output "Health check reported '$($response.status)', retrying..." + } + catch { + Write-Output "No response yet, retrying..." + } + + if ((Get-Date) -gt $deadline) { + Invoke-Docker "logs $containerName" + throw "RabbitMQ did not become healthy within 5 minutes." + } + + Start-Sleep -Seconds 5 + } + + Write-Output "RabbitMQ is healthy on $ipAddress" + Write-Output "::endgroup::" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a270209522..4b89792a1f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,8 +29,6 @@ jobs: exclude: - os: ubuntu-latest test-category: MSMQ - - os: windows-latest - test-category: IBMMQ fail-fast: false steps: - name: Check for secrets @@ -61,7 +59,11 @@ jobs: id: build background: true run: dotnet build tests.proj --configuration Release -graph + # Provisions the WSL2 Docker host that the SQL Server, PostgreSQL, RabbitMQ and IBM MQ containers + # run in on Windows. A no-op on Linux, but still run there so those actions see the same + # environment variables on both runners. - name: Setup WSL + if: startsWith(matrix.test-category, 'RabbitMQ') || contains(fromJSON('["SqlServer", "SqlServerPersistence", "PostgreSql", "PostgreSqlPersistence", "IBMMQ"]'), matrix.test-category) uses: Particular/setup-wsl-action@v1.0.0 # there is an issue with az cli and python 3.14, so for now we need to pin it @@ -80,9 +82,11 @@ jobs: python -m pip install --user "azure-cli==2.64.0" echo "$HOME/.local/bin" >> "$GITHUB_PATH" + # Only the Azure Service Bus and Azure Storage Queues actions provision cloud resources. RabbitMQ, + # PostgreSQL and SQL Server all run as containers now, so they no longer need an Azure session. - name: Azure login uses: azure/login@v3.0.1 - if: matrix.test-category == 'AzureServiceBus' || matrix.test-category == 'AzureStorageQueues' || startsWith(matrix.test-category, 'RabbitMQ') || matrix.test-category == 'PostgreSql' || matrix.test-category == 'PostgreSqlPersistence' + if: matrix.test-category == 'AzureServiceBus' || matrix.test-category == 'AzureStorageQueues' with: creds: ${{ secrets.AZURE_ACI_CREDENTIALS }} - name: Setup SQL Server @@ -113,11 +117,10 @@ jobs: registry-username: ${{ secrets.DOCKERHUB_USERNAME }} registry-password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Setup RabbitMQ - uses: Particular/setup-rabbitmq-action@v1.7.1 + uses: ./.github/actions/setup-rabbitmq if: startsWith(matrix.test-category, 'RabbitMQ') with: connection-string-name: ServiceControl_TransportTests_RabbitMQ_ConnectionString - tag: ServiceControl registry-username: ${{ secrets.DOCKERHUB_USERNAME }} registry-password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Setup Azure Service Bus @@ -135,17 +138,10 @@ jobs: azure-credentials: ${{ secrets.AZURE_ACI_CREDENTIALS }} tag: ServiceControl - name: Setup IBM MQ + uses: ./.github/actions/setup-ibmmq 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 utf-8 -Append + with: + connection-string-name: ServiceControl_TransportTests_IBMMQ_ConnectionString - name: Setup SQS environment variables if: matrix.test-category == 'SQS' run: | From a85eab7a427ada39bab86e362ce5058f3d19449a Mon Sep 17 00:00:00 2001 From: John Simons Date: Sat, 8 Aug 2026 15:48:02 +1000 Subject: [PATCH 4/8] Explicitly create all required event sources for multi-instance tests The `ServiceControl.MultiInstance.AcceptanceTests` now explicitly creates both the primary and audit event sources on Windows. This ensures the necessary EventLog sources are available for the tests. Previously, the audit event source was implicitly created by other acceptance tests, which is no longer guaranteed due to CI changes like parallelization. --- .../ServiceControl.Audit.csproj | 1 + .../SetupFixture.cs | 36 +++++++++++++------ 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/src/ServiceControl.Audit/ServiceControl.Audit.csproj b/src/ServiceControl.Audit/ServiceControl.Audit.csproj index f88c753d1b..a16a75c832 100644 --- a/src/ServiceControl.Audit/ServiceControl.Audit.csproj +++ b/src/ServiceControl.Audit/ServiceControl.Audit.csproj @@ -59,6 +59,7 @@ + diff --git a/src/ServiceControl.MultiInstance.AcceptanceTests/SetupFixture.cs b/src/ServiceControl.MultiInstance.AcceptanceTests/SetupFixture.cs index 4c74e8dcc9..a2b3d0c746 100644 --- a/src/ServiceControl.MultiInstance.AcceptanceTests/SetupFixture.cs +++ b/src/ServiceControl.MultiInstance.AcceptanceTests/SetupFixture.cs @@ -3,9 +3,12 @@ namespace ServiceControl.MultiInstance.AcceptanceTests; using System; using System.Diagnostics; using System.Runtime.InteropServices; +using System.Runtime.Versioning; using System.Threading; using System.Threading.Tasks; using NUnit.Framework; +using AuditEventSourceCreator = global::ServiceControl.Audit.Infrastructure.EventSourceCreator; +using PrimaryEventSourceCreator = ServiceBus.Management.Infrastructure.Installers.EventSourceCreator; [SetUpFixture] public class SetupFixture @@ -13,17 +16,30 @@ public class SetupFixture [OneTimeSetUp] public async Task Setup() { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - ServiceBus.Management.Infrastructure.Installers.EventSourceCreator.Create(); + return; + } + + // These tests host a primary and an audit instance, so both event sources have to exist before + // either instance runs its setup. The audit source used to be created as a side effect of the + // audit acceptance tests running earlier in the same CI job, which is no longer the case. + PrimaryEventSourceCreator.Create(); + AuditEventSourceCreator.Create(); + + await WaitForSource(PrimaryEventSourceCreator.SourceName); + await WaitForSource(AuditEventSourceCreator.SourceName); + } - //There is a delay for this becoming true, tests will fall over if they interleave in the wrong way. - var cts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); - while (!EventLog.SourceExists(ServiceBus.Management.Infrastructure.Installers.EventSourceCreator.SourceName)) - { - cts.Token.ThrowIfCancellationRequested(); - await Task.Delay(500, CancellationToken.None); - } + //There is a delay for this becoming true, tests will fall over if they interleave in the wrong way. + [SupportedOSPlatform("windows")] + static async Task WaitForSource(string sourceName) + { + var cts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); + while (!EventLog.SourceExists(sourceName)) + { + cts.Token.ThrowIfCancellationRequested(); + await Task.Delay(500, CancellationToken.None); } } -} \ No newline at end of file +} From e9275b84891ee2fec17e368f0cab53bed5f8ccc3 Mon Sep 17 00:00:00 2001 From: John Simons Date: Sat, 8 Aug 2026 16:07:30 +1000 Subject: [PATCH 5/8] Disable aggressive caching for notifications settings Notifications settings are rarely read and primarily edited by hand. Aggressive caching, which invalidates asynchronously via the Changes API, could lead to stale reads immediately following a save. This change ensures the latest settings are always retrieved, preventing potential consistency issues. --- .../Editing/NotificationsManager.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ServiceControl.Persistence.RavenDB/Editing/NotificationsManager.cs b/src/ServiceControl.Persistence.RavenDB/Editing/NotificationsManager.cs index 57e8d2b454..6531b6ed7b 100644 --- a/src/ServiceControl.Persistence.RavenDB/Editing/NotificationsManager.cs +++ b/src/ServiceControl.Persistence.RavenDB/Editing/NotificationsManager.cs @@ -1,6 +1,5 @@ namespace ServiceControl.Persistence.RavenDB.Editing { - using System; using System.Threading.Tasks; using Notifications; using Raven.Client.Documents.Session; @@ -8,11 +7,12 @@ class NotificationsManager(IAsyncDocumentSession session) : AbstractSessionManager(session), INotificationsManager { const string SingleDocumentId = "NotificationsSettings/All"; - static readonly TimeSpan CacheTimeout = TimeSpan.FromMinutes(5); // Raven requires this to be at least 1 second public async Task LoadSettings() { - using var aggressivelyCacheFor = await Session.Advanced.DocumentStore.AggressivelyCacheForAsync(CacheTimeout); + // Deliberately not aggressively cached. These settings are read rarely and edited by hand, + // and aggressive caching invalidates asynchronously via the Changes API, so a read straight + // after a save can return the pre-save document. var settings = await Session .LoadAsync(SingleDocumentId); From 38a27a797c50b8555039c777e26165e7eacf155d Mon Sep 17 00:00:00 2001 From: John Simons Date: Sat, 8 Aug 2026 16:51:27 +1000 Subject: [PATCH 6/8] Enhance CI stability for messaging service setup Improves the reliability of GitHub Actions for IBM MQ and RabbitMQ. IBM MQ's health check now directly probes the listener port, as Docker's internal check was insufficient for detecting connectivity issues, especially on Windows/WSL. RabbitMQ setup is made more robust by automatically restarting the container once if it hangs during boot within 90 seconds. Additionally, WSL memory allocation is increased to 8GB to provide more headroom and prevent resource-related failures during concurrent builds. --- .github/actions/setup-ibmmq/action.yml | 29 ++++++++++++++++------- .github/actions/setup-rabbitmq/action.yml | 17 +++++++++++-- .github/workflows/ci.yml | 4 ++++ 3 files changed, 40 insertions(+), 10 deletions(-) diff --git a/.github/actions/setup-ibmmq/action.yml b/.github/actions/setup-ibmmq/action.yml index 58bb5c11a6..7f581ad722 100644 --- a/.github/actions/setup-ibmmq/action.yml +++ b/.github/actions/setup-ibmmq/action.yml @@ -57,23 +57,36 @@ runs: "-e LICENSE=accept -e MQ_QMGR_NAME=$($Env:QUEUE_MANAGER) -e MQ_ADMIN_PASSWORD=$($Env:ADMIN_PASSWORD) " + $Env:IMAGE) - Write-Output "::group::Waiting for container health check" + # On Windows the queue manager listens inside the WSL VM, reachable on its gateway address. + $mqHost = if ($onWindows) { $Env:WSL_IP } else { 'localhost' } + + # Docker's health check runs dspmq inside the container, which reports the queue manager + # process rather than whether the listener is reachable through the WSL port forward. Probe + # the endpoint the tests actually connect to instead, the way setup-postgres-action does. + function Test-Endpoint([string]$TargetHost, [int]$Port) { + $client = [System.Net.Sockets.TcpClient]::new() + try { return $client.ConnectAsync($TargetHost, $Port).Wait(5000) -and $client.Connected } + catch { return $false } + finally { $client.Dispose() } + } + + Write-Output "::group::Waiting for IBM MQ to accept connections on ${mqHost}:1414" - # Out-String because the WSL and native paths differ in whether they hand back a string or a - # collection of output lines. $deadline = (Get-Date).AddMinutes(5) - while ((Invoke-Docker "inspect --format '{{.State.Health.Status}}' $containerName" | Out-String).Trim() -ne 'healthy') { + while (-not (Test-Endpoint $mqHost 1414)) { if ((Get-Date) -gt $deadline) { + # Out-String because the WSL and native paths differ in whether they hand back a string + # or a collection of output lines. + $status = (Invoke-Docker "inspect --format '{{.State.Status}} {{.State.Health.Status}}' $containerName" | Out-String).Trim() + Write-Output "Container state: $status" + Invoke-Docker "ps --all --filter name=$containerName" Invoke-Docker "logs $containerName" - throw "IBM MQ did not report healthy within 5 minutes." + throw "IBM MQ did not accept connections on ${mqHost}:1414 within 5 minutes." } Start-Sleep -Seconds 2 } Write-Output "::endgroup::" - - # On Windows the queue manager listens inside the WSL VM, reachable on its gateway address. - $mqHost = if ($onWindows) { $Env:WSL_IP } else { 'localhost' } $connectionString = "mq://admin:$($Env:ADMIN_PASSWORD)@${mqHost}:1414/$($Env:QUEUE_MANAGER)?channel=DEV.ADMIN.SVRCONN&topicprefix=DEV" Write-Output "IBM MQ is healthy on $mqHost" diff --git a/.github/actions/setup-rabbitmq/action.yml b/.github/actions/setup-rabbitmq/action.yml index 05f6ab0e39..b292f45f75 100644 --- a/.github/actions/setup-rabbitmq/action.yml +++ b/.github/actions/setup-rabbitmq/action.yml @@ -87,22 +87,35 @@ runs: $authHeader = @{ Authorization = 'Basic ' + [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes('guest:guest')) } $healthUri = "http://${ipAddress}:15672/api/health/checks/virtual-hosts" $deadline = (Get-Date).AddMinutes(5) + # The broker occasionally hangs part way through boot inside WSL, logging nothing further. One + # restart recovers it, and the log line below makes it visible when that happens. + $restartAfter = (Get-Date).AddSeconds(90) + $restarted = $false + $lastError = 'no attempt made yet' while ($true) { try { $response = Invoke-RestMethod -Uri $healthUri -Headers $authHeader -TimeoutSec 5 if ($response.status -eq 'ok') { break } - Write-Output "Health check reported '$($response.status)', retrying..." + $lastError = "health check reported '$($response.status)'" } catch { - Write-Output "No response yet, retrying..." + $lastError = $_.Exception.Message } if ((Get-Date) -gt $deadline) { + Write-Output "Last probe error: $lastError" + Invoke-Docker "ps --all --filter name=$containerName" Invoke-Docker "logs $containerName" throw "RabbitMQ did not become healthy within 5 minutes." } + if (-not $restarted -and (Get-Date) -gt $restartAfter) { + Write-Output "RabbitMQ has not responded after 90 seconds ($lastError). Restarting the container once." + Invoke-Docker "restart $containerName" + $restarted = $true + } + Start-Sleep -Seconds 5 } diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4b89792a1f..194b6fd360 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -65,6 +65,10 @@ jobs: - name: Setup WSL if: startsWith(matrix.test-category, 'RabbitMQ') || contains(fromJSON('["SqlServer", "SqlServerPersistence", "PostgreSql", "PostgreSqlPersistence", "IBMMQ"]'), matrix.test-category) uses: Particular/setup-wsl-action@v1.0.0 + with: + # The action defaults to 4GB. The runner has 16GB and the build runs concurrently with the + # container starting up, so give the VM real headroom. + memory: 8GB # 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 From 7e4a1ed105d20458b0d3911f7989768e6652893d Mon Sep 17 00:00:00 2001 From: John Simons Date: Sun, 9 Aug 2026 07:55:21 +1000 Subject: [PATCH 7/8] Optimize CI by compiling solution once and caching output Introduces a dedicated `compile` job that builds the entire solution on Linux and caches the resulting binaries. Subsequent test jobs now restore NuGet packages and download these pre-compiled artifacts instead of rebuilding. This significantly reduces redundant compilation across the test matrix, improving overall CI efficiency. The RavenDB server download is also centralized to this `compile` job. --- .github/workflows/ci.yml | 120 ++++++++++++++++++++++++--------------- 1 file changed, 75 insertions(+), 45 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 194b6fd360..d99662a3d3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,8 +12,62 @@ defaults: run: shell: pwsh jobs: + # Compiles the whole solution once, on Linux because it is the faster runner, and publishes the + # output for every test job to reuse. Only bin/ is cached: obj/ holds absolute paths into the Linux + # NuGet cache, so each test job regenerates it with a local `dotnet restore` instead. + compile: + name: Compile + runs-on: ubuntu-latest + outputs: + cache-key: ${{ steps.key.outputs.value }} + 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: Compute cache key + id: key + run: echo "value=build-output-${{ runner.os }}-${{ github.sha }}" | Out-File -FilePath $Env:GITHUB_OUTPUT -Encoding utf-8 -Append + # ServiceControlInstaller.Packaging's zip targets fail without a self-contained server in deploy/, + # so the full-solution build needs this and every test job waits behind it. The server is pinned + # to the RavenDB.Embedded version, so cache it rather than re-downloading ~18s per run. + - name: Cache RavenDB Server + id: raven + uses: actions/cache@v6.1.0 + with: + path: deploy/RavenDBServer + key: ravendb-server-${{ runner.os }}-${{ hashFiles('src/Directory.Packages.props') }} + - name: Download RavenDB Server + if: steps.raven.outputs.cache-hit != 'true' + run: ./tools/download-ravendb-server.ps1 + # EnableWindowsTargeting lets the net*-windows projects compile off Windows by restoring the + # targeting packs from NuGet. + - name: Build + run: dotnet build src --configuration Release -graph --property:EnableWindowsTargeting=true + # The headline number for this experiment: everything below is paid once here and then again, + # as a download, in every test job. + - name: Report build output size + run: | + $files = Get-ChildItem -Path src/*/bin -Recurse -File -ErrorAction SilentlyContinue + $gb = [math]::Round(($files | Measure-Object -Property Length -Sum).Sum / 1GB, 2) + Write-Output "Build output to cache: $gb GB across $($files.Count) files" + - name: Save build output to cache + uses: actions/cache/save@v6.1.0 + with: + path: src/*/bin + key: ${{ steps.key.outputs.value }} + build: name: ${{ matrix.os-name }}-${{ matrix.test-category }} + needs: compile runs-on: ${{ matrix.os }} strategy: matrix: @@ -46,19 +100,27 @@ jobs: - name: Select test projects id: select run: ./tools/select-test-projects.ps1 -Category ${{ matrix.test-category }} - # ServiceControlInstaller.Packaging is only in the DefaultCore category's build closure, and its - # zip targets fail without a self-contained server in deploy/. Tests never read it: - # RavenDB.Embedded supplies its own server into each test project's output directory. - - name: Download RavenDB Server - if: matrix.test-category == 'DefaultCore' - run: ./tools/download-ravendb-server.ps1 - # Backgrounded so that it overlaps the infrastructure steps below. Those steps are an ordered - # chain (Setup WSL provisions the Docker host the database actions rely on), so they stay in the - # foreground and the job joins the build at the `wait` before running tests. - - name: Build + # Both are backgrounded so they overlap the infrastructure steps below, which are an ordered + # chain (Setup WSL provisions the Docker host the database actions rely on). They are independent + # of each other: the cache supplies bin/, the restore regenerates obj/. Separate steps so the + # cache download time is attributable on its own. + - name: Download compiled output from cache + id: cache + background: true + uses: actions/cache/restore@v6.1.0 + with: + path: src/*/bin + key: ${{ needs.compile.outputs.cache-key }} + fail-on-cache-miss: true + # obj/ is deliberately not cached: it embeds absolute paths into the Linux runner's NuGet cache, + # so it has to be regenerated for this OS. dotnet test --no-build then uses the cached assemblies + # without recompiling. + - name: Restore NuGet packages id: build background: true - run: dotnet build tests.proj --configuration Release -graph + run: | + dotnet restore src + if ($LASTEXITCODE -ne 0) { throw "dotnet restore failed with exit code $LASTEXITCODE" } # Provisions the WSL2 Docker host that the SQL Server, PostgreSQL, RabbitMQ and IBM MQ containers # run in on Windows. A no-op on Linux, but still run there so those actions see the same # environment variables on both runners. @@ -156,8 +218,8 @@ jobs: # 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 - - name: Wait for build - wait: build + - name: Wait for compiled output and packages + wait: [cache, build] - name: Run tests run: ./tools/run-tests.ps1 -Projects $Env:TEST_PROJECTS env: @@ -166,38 +228,6 @@ jobs: PARTICULARSOFTWARE_LICENSE: ${{ secrets.LICENSETEXT }} AZURE_ACI_CREDENTIALS: ${{ secrets.AZURE_ACI_CREDENTIALS }} - # The test matrix only builds each category's own dependency closure, so this job is what keeps - # projects outside every closure (HealthCheckApp, Particular.PlatformSample.ServiceControl, - # LegacyArtifacts) compiling. It runs alongside the matrix and is not on its critical path. - compile: - name: ${{ matrix.os-name }}-Compile - runs-on: ${{ matrix.os }} - strategy: - matrix: - include: - - os: windows-latest - os-name: Windows - - os: ubuntu-latest - os-name: Linux - 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 - - name: Download RavenDB Server - run: ./tools/download-ravendb-server.ps1 - - name: Build - run: dotnet build src --configuration Release -graph - windows-installers: uses: ./.github/workflows/build-windows.yml secrets: inherit From 9d31c19cc6c1edb15649b87ca5daedd7d9592004 Mon Sep 17 00:00:00 2001 From: John Simons Date: Sun, 9 Aug 2026 08:21:41 +1000 Subject: [PATCH 8/8] Fix cross-OS build cache restoration and PowerShell path resolution Enables `enableCrossOsArchive` for the build output cache. This is crucial for Windows test jobs to successfully restore the cache, which is initially created on Linux. Without this setting, the cache archive is platform-specific, leading to consistent misses on Windows. Additionally, refactors the PowerShell script responsible for reporting build output size. The previous `Get-ChildItem -Path src/*/bin -Recurse` command did not correctly resolve the `bin` directories due to how PowerShell handles wildcards within the path argument, treating the leaf `bin` as a filter during recursion. The updated script explicitly resolves the `bin` directories first, ensuring accurate reporting and inclusion in the cache. --- .github/workflows/ci.yml | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2e814d86f4..46a2f113ac 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -67,14 +67,21 @@ jobs: # as a download, in every test job. - name: Report build output size run: | - $files = Get-ChildItem -Path src/*/bin -Recurse -File -ErrorAction SilentlyContinue + # Resolve the bin directories first. `Get-ChildItem -Path src/*/bin -Recurse` does not work: + # with a wildcard in the path, PowerShell treats the leaf as a filter and looks for items + # *named* bin during recursion, which matches nothing. + $binDirs = Get-ChildItem -Path src -Directory | ForEach-Object { Join-Path $_.FullName 'bin' } | Where-Object { Test-Path $_ } + $files = Get-ChildItem -Path $binDirs -Recurse -File -ErrorAction SilentlyContinue $gb = [math]::Round(($files | Measure-Object -Property Length -Sum).Sum / 1GB, 2) - Write-Output "Build output to cache: $gb GB across $($files.Count) files" + Write-Output "Build output to cache: $gb GB uncompressed across $($files.Count) files" - name: Save build output to cache uses: actions/cache/save@v6.1.0 with: path: src/*/bin key: ${{ steps.key.outputs.value }} + # Required for the Windows test jobs to restore a cache written on Linux. Without it the + # archive is platform-specific and every Windows restore misses. + enableCrossOsArchive: true build: name: ${{ matrix.os-name }}-${{ matrix.test-category }} @@ -123,6 +130,8 @@ jobs: path: src/*/bin key: ${{ needs.compile.outputs.cache-key }} fail-on-cache-miss: true + # Must match the save side: the cache is written on Linux and restored on both OSes. + enableCrossOsArchive: true # obj/ is deliberately not cached: it embeds absolute paths into the Linux runner's NuGet cache, # so it has to be regenerated for this OS. dotnet test --no-build then uses the cached assemblies # without recompiling.