Skip to content

Run CI and benchmark client workloads through native TUnit #9

Run CI and benchmark client workloads through native TUnit

Run CI and benchmark client workloads through native TUnit #9

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: |
node scripts/Features/TestInfrastructure/run-tests.mjs --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: node scripts/Features/TestInfrastructure/run-tests.mjs --KeyLoadTests:Suite=analyzers --KeyLoadTests:ReportTrx=true
- name: Test native coverage source ownership
id: source-ownership
run: node scripts/Features/TestInfrastructure/run-tests.mjs --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: node scripts/Features/TestInfrastructure/run-tests.mjs --KeyLoadTests:Suite=unit --KeyLoadTests:ReportTrx=true
- name: Run unit tests without CPU intrinsics
if: ${{ !cancelled() && steps.build.outcome == 'success' }}
run: node scripts/Features/TestInfrastructure/run-tests.mjs --KeyLoadTests:Suite=unit-scalar --KeyLoadTests:ReportTrx=true
- name: Test recovery after process crashes
if: ${{ !cancelled() && steps.build.outcome == 'success' }}
run: node scripts/Features/TestInfrastructure/run-tests.mjs --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: node scripts/Features/TestInfrastructure/run-tests.mjs --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 = @(
'scripts/Features/TestInfrastructure/run-tests.mjs',
'--KeyLoadTests:ReportTrx=true',
'--KeyLoadTests:CoverageSettings=scripts/Features/CodeQuality/functional-coverage.production.settings.xml',
'--KeyLoadTests:CoverageFormat=coverage')
& node @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
& node @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
& node @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
& node @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