Repository navigation
Keep RF3 image validation independent of benchmark Dockerfiles #7
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Build and Tests | |
| on: | |
| push: | |
| branches: [main] | |
| pull_request: | |
| workflow_dispatch: | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: keyload-build-tests-${{ github.event_name == 'pull_request' && github.ref || github.run_id }} | |
| cancel-in-progress: ${{ github.event_name == 'pull_request' }} | |
| jobs: | |
| repository-checks: | |
| name: Check repository rules | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| steps: | |
| - name: Download source code | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| - name: Check repository rules | |
| run: node scripts/Features/RepositoryGovernance/verify.mjs | |
| analyzer-rules: | |
| name: Test code analyzers | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - name: Download source code | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| - name: Set up .NET | |
| uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0 | |
| with: | |
| global-json-file: global.json | |
| - name: Restore .NET packages | |
| run: dotnet restore KeyLoad.slnx | |
| - name: Build analyzer tests | |
| run: | | |
| dotnet build src/KeyLoad.AppHost --no-restore --configuration Release | |
| dotnet build tests/KeyLoad.Analyzers.Tests --no-restore --configuration Release | |
| - name: Prepare source-bound analyzer coverage | |
| shell: pwsh | |
| run: | | |
| $root = $env:GITHUB_WORKSPACE | |
| $evidence = Join-Path $root 'artifacts/code-quality/analyzer-coverage' | |
| New-Item -ItemType Directory -Path $evidence -Force | Out-Null | |
| Copy-Item scripts/Features/CodeQuality/site-analyzer-coverage.settings.xml (Join-Path $evidence 'coverage.config.xml') | |
| & pwsh -NoLogo -NoProfile -File scripts/Features/CodeQuality/site-analyzer-coverage.ps1 ` | |
| -Mode Prepare -Repository $root ` | |
| -Contract (Join-Path $root 'scripts/Features/CodeQuality/site-analyzer-coverage.contract.json') ` | |
| -EvidenceRoot $evidence | |
| if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } | |
| - name: Run analyzer tests | |
| id: analyzers | |
| run: | | |
| dotnet run --project src/KeyLoad.AppHost --no-build --no-restore --configuration Release -- --KeyLoadTests:Suite=analyzers --KeyLoadTests:ResultsDirectory=TestResults/analyzers --KeyLoadTests:ReportTrx=true "--KeyLoadTests:CoverageSettings=$GITHUB_WORKSPACE/artifacts/code-quality/analyzer-coverage/coverage.config.xml" "--KeyLoadTests:CoverageOutput=$GITHUB_WORKSPACE/artifacts/code-quality/analyzer-coverage/coverage.cobertura.xml" | |
| - name: Verify every analyzer test passed | |
| if: ${{ !cancelled() && steps.analyzers.outcome != 'skipped' }} | |
| shell: pwsh | |
| run: | | |
| $root = $env:GITHUB_WORKSPACE | |
| $files = @(Get-ChildItem -LiteralPath (Join-Path $root 'TestResults/analyzers') -Filter '*.trx' -File) | |
| if ($files.Count -ne 1) { throw 'Expected exactly one analyzer TRX receipt' } | |
| [xml]$document = Get-Content -LiteralPath $files[0].FullName -Raw | |
| $counts = $document.TestRun.ResultSummary.Counters | |
| if ([int]$counts.total -le 0 -or [int]$counts.executed -ne [int]$counts.total -or | |
| [int]$counts.passed -ne [int]$counts.total) { | |
| throw 'Incomplete or failing analyzer qualification; skipped tests cannot pass' | |
| } | |
| [ordered]@{ | |
| sourceRevision = $env:GITHUB_SHA | |
| total = [int]$counts.total | |
| executed = [int]$counts.executed | |
| passed = [int]$counts.passed | |
| sha256 = (Get-FileHash -LiteralPath $files[0].FullName -Algorithm SHA256).Hash.ToLowerInvariant() | |
| } | ConvertTo-Json | Set-Content -LiteralPath (Join-Path $root 'artifacts/code-quality/analyzer-coverage/test-receipt.json') -Encoding utf8NoBOM | |
| - name: Verify source-bound analyzer coverage | |
| if: ${{ !cancelled() && steps.analyzers.outcome != 'skipped' }} | |
| shell: pwsh | |
| run: | | |
| $root = $env:GITHUB_WORKSPACE | |
| $evidence = Join-Path $root 'artifacts/code-quality/analyzer-coverage' | |
| & pwsh -NoLogo -NoProfile -File scripts/Features/CodeQuality/site-analyzer-coverage.ps1 ` | |
| -Mode Verify -Repository $root ` | |
| -Contract (Join-Path $root 'scripts/Features/CodeQuality/site-analyzer-coverage.contract.json') ` | |
| -EvidenceRoot $evidence -CoverageReport (Join-Path $evidence 'coverage.cobertura.xml') | |
| if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } | |
| - name: Save analyzer test results | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| if: always() | |
| with: | |
| name: analyzer-rule-evidence | |
| path: | | |
| TestResults/** | |
| tests/KeyLoad.Analyzers.Tests/**/TestResults/** | |
| artifacts/code-quality/** | |
| if-no-files-found: error | |
| verify: | |
| name: Build and test KeyLoad | |
| needs: repository-checks | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| os: [ubuntu-latest] | |
| runs-on: ${{ matrix.os }} | |
| timeout-minutes: 120 | |
| steps: | |
| - name: Download source code | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| fetch-depth: 0 | |
| - name: Set up .NET | |
| uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0 | |
| with: | |
| global-json-file: global.json | |
| - name: Restore .NET packages | |
| run: dotnet restore KeyLoad.slnx | |
| - name: Build KeyLoad | |
| id: build | |
| run: dotnet build KeyLoad.slnx --no-restore --configuration Release | |
| - name: Verify the pinned native full-text package | |
| run: | | |
| node --input-type=module <<'JS' | |
| import fs from 'node:fs'; | |
| import path from 'node:path'; | |
| import crypto from 'node:crypto'; | |
| import { execFileSync, spawnSync } from 'node:child_process'; | |
| const cache = execFileSync('dotnet', ['nuget', 'locals', 'global-packages', '--list'], { encoding: 'utf8' }).trim(); | |
| const prefix = 'global-packages: '; | |
| if (!cache.startsWith(prefix) || cache.includes('\n')) throw new Error('Ambiguous NuGet package cache.'); | |
| const packagePath = path.join(cache.slice(prefix.length), 'zonetree.fulltextsearch', '1.0.9', 'zonetree.fulltextsearch.1.0.9.nupkg'); | |
| const digest = crypto.createHash('sha256').update(fs.readFileSync(packagePath)).digest('hex'); | |
| if (digest !== '7ea1fbb7aba0ad00391d78d2f414166b500335f5b1affe43c305d861b55719ff') throw new Error('Native FTS package differs from ADR-078.'); | |
| const verification = spawnSync('dotnet', ['nuget', 'verify', packagePath, '--all'], { encoding: 'utf8', maxBuffer: 1048576 }); | |
| fs.mkdirSync('artifacts/qualification', { recursive: true }); | |
| fs.writeFileSync('artifacts/qualification/native-full-text-package-signature.log', `Package SHA256: ${digest}\n${verification.stdout ?? ''}${verification.stderr ?? ''}`); | |
| process.stdout.write(verification.stdout ?? ''); | |
| process.stderr.write(verification.stderr ?? ''); | |
| if (verification.error || verification.status !== 0) throw verification.error ?? new Error('Native FTS package signature verification failed.'); | |
| JS | |
| - name: Build current Debug analyzer for native formatting | |
| run: dotnet build src/KeyLoad.Analyzers/KeyLoad.Analyzers.csproj --no-restore --configuration Debug | |
| - name: Check code formatting | |
| run: dotnet format KeyLoad.slnx --verify-no-changes --no-restore | |
| - name: Check repository rules | |
| run: node scripts/Features/RepositoryGovernance/verify.mjs | |
| - name: Test code analyzers | |
| run: dotnet run --project src/KeyLoad.AppHost --no-build --no-restore --configuration Release -- --KeyLoadTests:Suite=analyzers --KeyLoadTests:ReportTrx=true | |
| - name: Test native coverage source ownership | |
| id: source-ownership | |
| run: dotnet run --project src/KeyLoad.AppHost --no-build --no-restore --configuration Release -- --KeyLoadTests:Suite=unit '--KeyLoadTests:Filter=/*/*/(NativePathMapCompilerTests)|(ProductionSourceManifestSettlementTests)|(NativeSourceManifestChildSettlementTests)|(ProductionSourceManifestOperationTests)/*' --KeyLoadTests:ResultsDirectory=TestResults/native-source-ownership --KeyLoadTests:ReportTrx=true | |
| - name: Verify original source ownership test receipt | |
| if: ${{ !cancelled() && steps.source-ownership.outcome != 'skipped' }} | |
| shell: pwsh | |
| run: | | |
| $ErrorActionPreference = 'Stop' | |
| Set-StrictMode -Version Latest | |
| $reports = @(Get-ChildItem TestResults/native-source-ownership -Filter '*.trx' -Recurse -File) | |
| if ($reports.Count -ne 1) { throw 'Exactly one original source ownership TRX is required.' } | |
| [xml] $trx = Get-Content -LiteralPath $reports[0].FullName -Raw | |
| $counters = $trx.TestRun.ResultSummary.Counters | |
| if ([int] $counters.total -ne 10 -or [int] $counters.executed -ne 10 -or | |
| [int] $counters.passed -ne 10 -or [int] $counters.failed -ne 0) { | |
| throw 'Every native source ownership case must execute and pass.' | |
| } | |
| $receipt = [ordered]@{ | |
| source = $env:GITHUB_SHA; total = 10; executed = 10; passed = 10 | |
| originalTrxSha256 = (Get-FileHash -LiteralPath $reports[0].FullName -Algorithm SHA256).Hash.ToLowerInvariant() | |
| } | |
| $receipt | ConvertTo-Json | Set-Content TestResults/native-source-ownership/test-receipt.json | |
| - name: Run unit tests | |
| if: ${{ !cancelled() && steps.build.outcome == 'success' }} | |
| run: dotnet run --project src/KeyLoad.AppHost --no-build --no-restore --configuration Release -- --KeyLoadTests:Suite=unit --KeyLoadTests:ReportTrx=true | |
| - name: Run unit tests without CPU intrinsics | |
| if: ${{ !cancelled() && steps.build.outcome == 'success' }} | |
| run: dotnet run --project src/KeyLoad.AppHost --no-build --no-restore --configuration Release -- --KeyLoadTests:Suite=unit-scalar --KeyLoadTests:ReportTrx=true | |
| - name: Test recovery after process crashes | |
| if: ${{ !cancelled() && steps.build.outcome == 'success' }} | |
| run: dotnet run --project src/KeyLoad.AppHost --no-build --no-restore --configuration Release -- --KeyLoadTests:Suite=recovery --KeyLoadTests:ReportTrx=true | |
| - name: Save test results | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| if: always() | |
| with: | |
| name: test-results-${{ matrix.os }} | |
| path: | | |
| **/TestResults/** | |
| artifacts/qualification/** | |
| if-no-files-found: ignore | |
| - name: Save build diagnostics | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| if: always() | |
| with: | |
| name: code-quality-${{ matrix.os }} | |
| path: artifacts/code-quality/** | |
| if-no-files-found: error | |
| docker-rf3: | |
| name: Test three-node database | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 60 | |
| steps: | |
| - name: Download source code | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| - name: Set up .NET | |
| uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0 | |
| with: | |
| global-json-file: global.json | |
| - name: Check Docker | |
| run: docker version | |
| - name: Build KeyLoad server Docker image | |
| id: images | |
| run: node scripts/Features/BenchmarkComparisons/prepare-images.mjs --server-only | |
| - name: Configure Docker image references | |
| shell: bash | |
| env: | |
| KEYLOAD_SERVER_IMAGE: ${{ steps.images.outputs.server-image }} | |
| run: | | |
| test -n "$KEYLOAD_SERVER_IMAGE" | |
| printf 'KeyLoad__ContainerImages__Server=%s\nKEYLOAD_IMAGE_RECEIPT=%s\n' "$KEYLOAD_SERVER_IMAGE" "$RUNNER_TEMP/keyload-images/image-receipt.json" >> "$GITHUB_ENV" | |
| - name: Find Chrome for admin tests | |
| shell: bash | |
| run: | | |
| admin_chrome=$(command -v google-chrome || command -v google-chrome-stable || command -v chromium || command -v chromium-browser) | |
| test -n "$admin_chrome" && test -x "$admin_chrome" | |
| "$admin_chrome" --version | |
| printf 'KEYLOAD_ADMIN_CHROME_PATH=%s\n' "$admin_chrome" >> "$GITHUB_ENV" | |
| - name: Restore .NET packages | |
| run: dotnet restore KeyLoad.slnx | |
| - name: Build the complete native coverage cohort | |
| run: dotnet build KeyLoad.slnx --no-restore --configuration Release | |
| - name: Prepare original source and test-image receipts | |
| shell: pwsh | |
| run: | | |
| $root = $env:GITHUB_WORKSPACE | |
| $evidence = Join-Path $root 'TestResults/functional-coverage/rf3' | |
| & pwsh -NoLogo -NoProfile -File scripts/Features/CodeQuality/functional-coverage.production-source-manifest.ps1 ` | |
| -Mode prepare -Root $root -EvidenceRoot $evidence | |
| if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } | |
| "KEYLOAD_PRODUCT_COVERAGE_SOURCE_MANIFEST=$evidence/functional-coverage.production-source-manifest.json" | Add-Content $env:GITHUB_ENV | |
| - name: Test three-node database with .NET and MCP clients | |
| run: dotnet run --project src/KeyLoad.AppHost --no-build --no-restore --configuration Release -- --KeyLoadTests:Suite=rf3 --KeyLoadTests:ResultsDirectory=TestResults/rf3-required --KeyLoadTests:ReportTrx=true | |
| - name: Collect four native functional coverage cohorts | |
| id: functional-coverage | |
| shell: pwsh | |
| run: | | |
| $common = @( | |
| 'run', '--project', 'src/KeyLoad.AppHost', '--no-build', '--no-restore', '--configuration', 'Release', '--', | |
| '--KeyLoadTests:ReportTrx=true', | |
| '--KeyLoadTests:CoverageSettings=scripts/Features/CodeQuality/functional-coverage.production.settings.xml', | |
| '--KeyLoadTests:CoverageFormat=coverage') | |
| & dotnet @common '--KeyLoadTests:Suite=unit' '--KeyLoadTests:Filter=/*/*/PartitionQuery*/*' ` | |
| '--KeyLoadTests:ResultsDirectory=TestResults/functional-coverage/rf3/unit' ` | |
| '--KeyLoadTests:CoverageOutput=TestResults/functional-coverage/rf3/unit/coverage.coverage' | |
| $unitExitCode = $LASTEXITCODE | |
| "unit_exit_code=$unitExitCode" | Add-Content $env:GITHUB_OUTPUT | |
| & dotnet @common '--KeyLoadTests:Suite=unit-scalar' '--KeyLoadTests:Filter=/*/*/PartitionQuery*/*' ` | |
| '--KeyLoadTests:ResultsDirectory=TestResults/functional-coverage/rf3/unit-scalar' ` | |
| '--KeyLoadTests:CoverageOutput=TestResults/functional-coverage/rf3/unit-scalar/coverage.coverage' | |
| $scalarExitCode = $LASTEXITCODE | |
| "scalar_exit_code=$scalarExitCode" | Add-Content $env:GITHUB_OUTPUT | |
| & dotnet @common '--KeyLoadTests:Suite=recovery' ` | |
| '--KeyLoadTests:Filter=/*/*/CommandIdempotencyProcessRecoveryTests/*' ` | |
| '--KeyLoadTests:ResultsDirectory=TestResults/functional-coverage/rf3/recovery' ` | |
| '--KeyLoadTests:CoverageOutput=TestResults/functional-coverage/rf3/recovery/coverage.coverage' | |
| $recoveryExitCode = $LASTEXITCODE | |
| "recovery_exit_code=$recoveryExitCode" | Add-Content $env:GITHUB_OUTPUT | |
| & dotnet @common '--KeyLoadTests:Suite=rf3' ` | |
| '--KeyLoadTests:Filter=/*/*/(PartitionQueryPublicRf3Tests)|(McpDocumentCrudParityTests)/*' ` | |
| '--KeyLoadTests:ResultsDirectory=TestResults/functional-coverage/rf3' ` | |
| '--KeyLoadTests:CoverageOutput=TestResults/functional-coverage/rf3/coverage.coverage' ` | |
| '--KeyLoadTests:NativeCoverage:ServerMode=rf3-original-node-v1' ` | |
| "--KeyLoadTests:NativeCoverage:SourceManifest=$env:KEYLOAD_PRODUCT_COVERAGE_SOURCE_MANIFEST" | |
| $rf3ExitCode = $LASTEXITCODE | |
| "rf3_exit_code=$rf3ExitCode" | Add-Content $env:GITHUB_OUTPUT | |
| if ($unitExitCode -ne 0 -or $scalarExitCode -ne 0 -or $recoveryExitCode -ne 0 -or $rf3ExitCode -ne 0) { | |
| exit 1 | |
| } | |
| - name: Assemble product descriptor from original reports | |
| id: product-descriptor | |
| if: ${{ success() }} | |
| shell: pwsh | |
| env: | |
| UNIT_EXIT_CODE: ${{ steps.functional-coverage.outputs.unit_exit_code }} | |
| SCALAR_EXIT_CODE: ${{ steps.functional-coverage.outputs.scalar_exit_code }} | |
| RECOVERY_EXIT_CODE: ${{ steps.functional-coverage.outputs.recovery_exit_code }} | |
| RF3_EXIT_CODE: ${{ steps.functional-coverage.outputs.rf3_exit_code }} | |
| run: | | |
| & pwsh -NoLogo -NoProfile -File scripts/Features/CodeQuality/functional-coverage.native-product-descriptor.ps1 ` | |
| -Repository $env:GITHUB_WORKSPACE ` | |
| -SourceManifestPath $env:KEYLOAD_PRODUCT_COVERAGE_SOURCE_MANIFEST ` | |
| -ResultsRoot (Join-Path $env:GITHUB_WORKSPACE 'TestResults/functional-coverage/rf3') ` | |
| -UnitExitCode ([int]$env:UNIT_EXIT_CODE) -ScalarExitCode ([int]$env:SCALAR_EXIT_CODE) ` | |
| -RecoveryExitCode ([int]$env:RECOVERY_EXIT_CODE) -Rf3ExitCode ([int]$env:RF3_EXIT_CODE) | |
| if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } | |
| - name: Validate and merge native product coverage | |
| if: ${{ success() }} | |
| shell: pwsh | |
| run: | | |
| $root = $env:GITHUB_WORKSPACE | |
| $evidence = Join-Path $root 'TestResults/functional-coverage/rf3' | |
| $descriptorPath = Join-Path $evidence 'functional-coverage.native-product-descriptor.v1.json' | |
| $boundsPath = Join-Path $evidence 'functional-coverage.native-options.v1.json' | |
| $descriptor = Get-Content -LiteralPath $descriptorPath -Raw | ConvertFrom-Json | |
| $nativeOptions = Get-Content -LiteralPath $boundsPath -Raw | |
| & pwsh -NoLogo -NoProfile -File scripts/Features/CodeQuality/functional-coverage.native-merge.ps1 ` | |
| -Mode Product -Repository $root -EvidenceRoot $evidence -DescriptorPath $descriptorPath ` | |
| -ToolPackageRoot $descriptor.tool.packageRoot -ToolVersion $descriptor.tool.version ` | |
| -NativeOptionsJson $nativeOptions | |
| if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } | |
| - name: Clean up Docker registry | |
| if: ${{ always() && (steps.images.outcome == 'success' || steps.images.outcome == 'failure') }} | |
| run: node scripts/Features/BenchmarkComparisons/cleanup-images.mjs | |
| - name: Save three-node Docker image logs | |
| if: ${{ always() && (steps.images.outcome == 'success' || steps.images.outcome == 'failure') }} | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: docker-rf3-image-evidence | |
| path: ${{ runner.temp }}/keyload-images/** | |
| if-no-files-found: error | |
| - name: Save three-node database test results | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| if: always() | |
| with: | |
| name: docker-rf3-qualification | |
| path: | | |
| TestResults/** | |
| tests/KeyLoad.IntegrationTests/**/TestResults/** | |
| artifacts/qualification/** | |
| if-no-files-found: error | |
| - name: Save three-node build diagnostics | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| if: always() | |
| with: | |
| name: code-quality-docker-rf3 | |
| path: artifacts/code-quality/** | |
| if-no-files-found: error |