Repository navigation
Repair native recovery and RF3 lifecycle evidence #11
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: | | |
| 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 |