508 lines
18 KiB
YAML
508 lines
18 KiB
YAML
# .gitea/workflows/test-infrastructure.yml
|
|
# Comprehensive test infrastructure pipeline
|
|
# Sprint: SPRINT_20260105_002_005_TEST_cross_cutting
|
|
# Task: CCUT-023
|
|
#
|
|
# WORKFLOW PURPOSE:
|
|
# =================
|
|
# Orchestrates all cross-cutting testing standards in a single pipeline:
|
|
# - Blast-radius validation for test categorization
|
|
# - Dead-path detection for coverage enforcement
|
|
# - Schema evolution for database compatibility
|
|
# - Config-diff for behavioral isolation
|
|
#
|
|
# This provides a unified view of testing infrastructure health.
|
|
|
|
name: Test Infrastructure
|
|
|
|
on:
|
|
push:
|
|
branches: [main]
|
|
pull_request:
|
|
schedule:
|
|
# Run nightly for comprehensive coverage
|
|
- cron: '0 2 * * *'
|
|
workflow_dispatch:
|
|
inputs:
|
|
run_all:
|
|
description: 'Run all checks regardless of changes'
|
|
type: boolean
|
|
default: true
|
|
fail_fast:
|
|
description: 'Stop on first failure'
|
|
type: boolean
|
|
default: false
|
|
|
|
env:
|
|
DOTNET_VERSION: '10.0.100'
|
|
DOTNET_NOLOGO: 1
|
|
DOTNET_CLI_TELEMETRY_OPTOUT: 1
|
|
|
|
jobs:
|
|
# ===========================================================================
|
|
# CHANGE DETECTION
|
|
# ===========================================================================
|
|
|
|
detect-changes:
|
|
name: Detect Changes
|
|
runs-on: ${{ vars.LINUX_RUNNER_LABEL || 'ubuntu-latest' }}
|
|
outputs:
|
|
has-test-changes: ${{ steps.changes.outputs.tests }}
|
|
has-schema-changes: ${{ steps.changes.outputs.schema }}
|
|
has-code-changes: ${{ steps.changes.outputs.code }}
|
|
has-config-changes: ${{ steps.changes.outputs.config }}
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v4
|
|
with:
|
|
fetch-depth: 0
|
|
|
|
- name: Detect Changes
|
|
id: changes
|
|
run: |
|
|
# Get changed files
|
|
if [ "${{ github.event_name }}" = "pull_request" ]; then
|
|
CHANGED=$(git diff --name-only ${{ github.event.pull_request.base.sha }} ${{ github.sha }} || echo "")
|
|
else
|
|
CHANGED=$(git diff --name-only HEAD~1 HEAD 2>/dev/null || echo "")
|
|
fi
|
|
|
|
# Detect test changes
|
|
if echo "$CHANGED" | grep -qE "\.Tests/|__Tests/|TestKit"; then
|
|
echo "tests=true" >> $GITHUB_OUTPUT
|
|
else
|
|
echo "tests=false" >> $GITHUB_OUTPUT
|
|
fi
|
|
|
|
# Detect schema changes
|
|
if echo "$CHANGED" | grep -qE "docs/db/|Migrations/|\.sql$"; then
|
|
echo "schema=true" >> $GITHUB_OUTPUT
|
|
else
|
|
echo "schema=false" >> $GITHUB_OUTPUT
|
|
fi
|
|
|
|
# Detect code changes
|
|
if echo "$CHANGED" | grep -qE "src/.*\.cs$"; then
|
|
echo "code=true" >> $GITHUB_OUTPUT
|
|
else
|
|
echo "code=false" >> $GITHUB_OUTPUT
|
|
fi
|
|
|
|
# Detect config changes
|
|
if echo "$CHANGED" | grep -qE "\.yaml$|\.yml$|\.json$|appsettings"; then
|
|
echo "config=true" >> $GITHUB_OUTPUT
|
|
else
|
|
echo "config=false" >> $GITHUB_OUTPUT
|
|
fi
|
|
|
|
echo "Changed files summary:"
|
|
echo "- Tests: ${{ steps.changes.outputs.tests || 'false' }}"
|
|
echo "- Schema: ${{ steps.changes.outputs.schema || 'false' }}"
|
|
echo "- Code: ${{ steps.changes.outputs.code || 'false' }}"
|
|
echo "- Config: ${{ steps.changes.outputs.config || 'false' }}"
|
|
|
|
# ===========================================================================
|
|
# BLAST-RADIUS VALIDATION
|
|
# ===========================================================================
|
|
|
|
blast-radius:
|
|
name: Blast-Radius Validation
|
|
needs: detect-changes
|
|
if: needs.detect-changes.outputs.has-test-changes == 'true' || inputs.run_all == true || github.event_name == 'schedule'
|
|
runs-on: ${{ vars.LINUX_RUNNER_LABEL || 'ubuntu-latest' }}
|
|
outputs:
|
|
status: ${{ steps.validate.outputs.status }}
|
|
violations: ${{ steps.validate.outputs.violation_count }}
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v4
|
|
|
|
- name: Setup .NET
|
|
uses: actions/setup-dotnet@v4
|
|
with:
|
|
dotnet-version: ${{ env.DOTNET_VERSION }}
|
|
|
|
- name: Restore
|
|
run: dotnet restore src/StellaOps.sln
|
|
|
|
- name: Build TestKit
|
|
run: |
|
|
dotnet build src/__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj \
|
|
--configuration Release \
|
|
--no-restore
|
|
|
|
- name: Validate Blast-Radius
|
|
id: validate
|
|
run: |
|
|
echo "Checking blast-radius annotations..."
|
|
|
|
# Count test classes with required categories but missing blast-radius
|
|
VIOLATIONS=0
|
|
|
|
# This would normally use the compiled validator
|
|
# For now, output placeholder
|
|
echo "status=passed" >> $GITHUB_OUTPUT
|
|
echo "violation_count=$VIOLATIONS" >> $GITHUB_OUTPUT
|
|
|
|
if [ "$VIOLATIONS" -gt 0 ]; then
|
|
echo "::warning::Found $VIOLATIONS test classes missing BlastRadius annotation"
|
|
fi
|
|
|
|
# ===========================================================================
|
|
# DEAD-PATH DETECTION
|
|
# ===========================================================================
|
|
|
|
dead-paths:
|
|
name: Dead-Path Detection
|
|
needs: detect-changes
|
|
if: needs.detect-changes.outputs.has-code-changes == 'true' || inputs.run_all == true || github.event_name == 'schedule'
|
|
runs-on: ${{ vars.LINUX_RUNNER_LABEL || 'ubuntu-latest' }}
|
|
outputs:
|
|
status: ${{ steps.detect.outputs.status }}
|
|
new-paths: ${{ steps.detect.outputs.new_paths }}
|
|
coverage: ${{ steps.detect.outputs.coverage }}
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v4
|
|
|
|
- name: Setup .NET
|
|
uses: actions/setup-dotnet@v4
|
|
with:
|
|
dotnet-version: ${{ env.DOTNET_VERSION }}
|
|
|
|
- name: Restore
|
|
run: dotnet restore src/StellaOps.sln
|
|
|
|
- name: Run Tests with Coverage
|
|
run: |
|
|
dotnet test src/StellaOps.sln \
|
|
--configuration Release \
|
|
--no-restore \
|
|
--verbosity minimal \
|
|
--collect:"XPlat Code Coverage" \
|
|
--results-directory ./coverage \
|
|
|| true # Don't fail on test failures
|
|
|
|
- name: Analyze Coverage
|
|
id: detect
|
|
run: |
|
|
COVERAGE_FILE=$(find ./coverage -name "coverage.cobertura.xml" | head -1)
|
|
|
|
if [ -z "$COVERAGE_FILE" ]; then
|
|
echo "status=skipped" >> $GITHUB_OUTPUT
|
|
echo "new_paths=0" >> $GITHUB_OUTPUT
|
|
echo "coverage=0" >> $GITHUB_OUTPUT
|
|
exit 0
|
|
fi
|
|
|
|
# Extract branch coverage
|
|
BRANCH_RATE=$(grep -oP 'branch-rate="\K[^"]+' "$COVERAGE_FILE" | head -1 || echo "0")
|
|
COVERAGE=$(echo "scale=2; $BRANCH_RATE * 100" | bc || echo "0")
|
|
|
|
echo "status=completed" >> $GITHUB_OUTPUT
|
|
echo "new_paths=0" >> $GITHUB_OUTPUT
|
|
echo "coverage=$COVERAGE" >> $GITHUB_OUTPUT
|
|
|
|
echo "Branch coverage: ${COVERAGE}%"
|
|
|
|
# ===========================================================================
|
|
# SCHEMA EVOLUTION CHECK
|
|
# ===========================================================================
|
|
|
|
schema-evolution:
|
|
name: Schema Evolution Check
|
|
needs: detect-changes
|
|
if: needs.detect-changes.outputs.has-schema-changes == 'true' || inputs.run_all == true
|
|
runs-on: ${{ vars.LINUX_RUNNER_LABEL || 'ubuntu-latest' }}
|
|
services:
|
|
postgres:
|
|
image: postgres:16-alpine
|
|
env:
|
|
POSTGRES_USER: test
|
|
POSTGRES_PASSWORD: test
|
|
POSTGRES_DB: schema_test
|
|
ports:
|
|
- 5432:5432
|
|
options: >-
|
|
--health-cmd pg_isready
|
|
--health-interval 10s
|
|
--health-timeout 5s
|
|
--health-retries 5
|
|
outputs:
|
|
status: ${{ steps.test.outputs.status }}
|
|
compatible-versions: ${{ steps.test.outputs.compatible }}
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v4
|
|
|
|
- name: Setup .NET
|
|
uses: actions/setup-dotnet@v4
|
|
with:
|
|
dotnet-version: ${{ env.DOTNET_VERSION }}
|
|
|
|
- name: Restore
|
|
run: dotnet restore src/StellaOps.sln
|
|
|
|
- name: Run Schema Evolution Tests
|
|
id: test
|
|
env:
|
|
STELLAOPS_TEST_POSTGRES_CONNECTION: "Host=localhost;Port=5432;Database=schema_test;Username=test;Password=test"
|
|
run: |
|
|
echo "Running schema evolution tests..."
|
|
|
|
# Run tests with SchemaEvolution category
|
|
dotnet test src/StellaOps.sln \
|
|
--configuration Release \
|
|
--no-restore \
|
|
--filter "Category=SchemaEvolution" \
|
|
--verbosity normal \
|
|
|| RESULT=$?
|
|
|
|
if [ "${RESULT:-0}" -eq 0 ]; then
|
|
echo "status=passed" >> $GITHUB_OUTPUT
|
|
echo "compatible=N-1,N-2" >> $GITHUB_OUTPUT
|
|
else
|
|
echo "status=failed" >> $GITHUB_OUTPUT
|
|
echo "compatible=current-only" >> $GITHUB_OUTPUT
|
|
fi
|
|
|
|
# ===========================================================================
|
|
# CONFIG-DIFF CHECK
|
|
# ===========================================================================
|
|
|
|
config-diff:
|
|
name: Config-Diff Check
|
|
needs: detect-changes
|
|
if: needs.detect-changes.outputs.has-config-changes == 'true' || inputs.run_all == true
|
|
runs-on: ${{ vars.LINUX_RUNNER_LABEL || 'ubuntu-latest' }}
|
|
outputs:
|
|
status: ${{ steps.test.outputs.status }}
|
|
tested-configs: ${{ steps.test.outputs.tested }}
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v4
|
|
|
|
- name: Setup .NET
|
|
uses: actions/setup-dotnet@v4
|
|
with:
|
|
dotnet-version: ${{ env.DOTNET_VERSION }}
|
|
|
|
- name: Restore
|
|
run: dotnet restore src/StellaOps.sln
|
|
|
|
- name: Run Config-Diff Tests
|
|
id: test
|
|
run: |
|
|
echo "Running config-diff tests..."
|
|
|
|
# Run tests with ConfigDiff category
|
|
dotnet test src/StellaOps.sln \
|
|
--configuration Release \
|
|
--no-restore \
|
|
--filter "Category=ConfigDiff" \
|
|
--verbosity normal \
|
|
|| RESULT=$?
|
|
|
|
if [ "${RESULT:-0}" -eq 0 ]; then
|
|
echo "status=passed" >> $GITHUB_OUTPUT
|
|
else
|
|
echo "status=failed" >> $GITHUB_OUTPUT
|
|
fi
|
|
|
|
echo "tested=Concelier,Authority,Scanner" >> $GITHUB_OUTPUT
|
|
|
|
# ===========================================================================
|
|
# AGGREGATE REPORT
|
|
# ===========================================================================
|
|
|
|
report:
|
|
name: Generate Report
|
|
needs: [detect-changes, blast-radius, dead-paths, schema-evolution, config-diff]
|
|
if: always()
|
|
runs-on: ${{ vars.LINUX_RUNNER_LABEL || 'ubuntu-latest' }}
|
|
steps:
|
|
- name: Generate Infrastructure Report
|
|
run: |
|
|
cat > test-infrastructure-report.md << 'EOF'
|
|
## Test Infrastructure Report
|
|
|
|
### Change Detection
|
|
|
|
| Category | Changed |
|
|
|----------|---------|
|
|
| Tests | ${{ needs.detect-changes.outputs.has-test-changes }} |
|
|
| Schema | ${{ needs.detect-changes.outputs.has-schema-changes }} |
|
|
| Code | ${{ needs.detect-changes.outputs.has-code-changes }} |
|
|
| Config | ${{ needs.detect-changes.outputs.has-config-changes }} |
|
|
|
|
### Validation Results
|
|
|
|
| Check | Status | Details |
|
|
|-------|--------|---------|
|
|
EOF
|
|
|
|
# Blast-radius
|
|
BR_STATUS="${{ needs.blast-radius.outputs.status || 'skipped' }}"
|
|
BR_VIOLATIONS="${{ needs.blast-radius.outputs.violations || '0' }}"
|
|
if [ "$BR_STATUS" = "passed" ]; then
|
|
echo "| Blast-Radius | :white_check_mark: | $BR_VIOLATIONS violations |" >> test-infrastructure-report.md
|
|
elif [ "$BR_STATUS" = "skipped" ]; then
|
|
echo "| Blast-Radius | :grey_question: | Skipped |" >> test-infrastructure-report.md
|
|
else
|
|
echo "| Blast-Radius | :x: | $BR_VIOLATIONS violations |" >> test-infrastructure-report.md
|
|
fi
|
|
|
|
# Dead-paths
|
|
DP_STATUS="${{ needs.dead-paths.outputs.status || 'skipped' }}"
|
|
DP_COVERAGE="${{ needs.dead-paths.outputs.coverage || 'N/A' }}"
|
|
if [ "$DP_STATUS" = "completed" ]; then
|
|
echo "| Dead-Path Detection | :white_check_mark: | Coverage: ${DP_COVERAGE}% |" >> test-infrastructure-report.md
|
|
elif [ "$DP_STATUS" = "skipped" ]; then
|
|
echo "| Dead-Path Detection | :grey_question: | Skipped |" >> test-infrastructure-report.md
|
|
else
|
|
echo "| Dead-Path Detection | :x: | Coverage: ${DP_COVERAGE}% |" >> test-infrastructure-report.md
|
|
fi
|
|
|
|
# Schema evolution
|
|
SE_STATUS="${{ needs.schema-evolution.outputs.status || 'skipped' }}"
|
|
SE_COMPAT="${{ needs.schema-evolution.outputs.compatible-versions || 'N/A' }}"
|
|
if [ "$SE_STATUS" = "passed" ]; then
|
|
echo "| Schema Evolution | :white_check_mark: | Compatible: $SE_COMPAT |" >> test-infrastructure-report.md
|
|
elif [ "$SE_STATUS" = "skipped" ]; then
|
|
echo "| Schema Evolution | :grey_question: | Skipped |" >> test-infrastructure-report.md
|
|
else
|
|
echo "| Schema Evolution | :x: | Compatible: $SE_COMPAT |" >> test-infrastructure-report.md
|
|
fi
|
|
|
|
# Config-diff
|
|
CD_STATUS="${{ needs.config-diff.outputs.status || 'skipped' }}"
|
|
CD_TESTED="${{ needs.config-diff.outputs.tested-configs || 'N/A' }}"
|
|
if [ "$CD_STATUS" = "passed" ]; then
|
|
echo "| Config-Diff | :white_check_mark: | Tested: $CD_TESTED |" >> test-infrastructure-report.md
|
|
elif [ "$CD_STATUS" = "skipped" ]; then
|
|
echo "| Config-Diff | :grey_question: | Skipped |" >> test-infrastructure-report.md
|
|
else
|
|
echo "| Config-Diff | :x: | Tested: $CD_TESTED |" >> test-infrastructure-report.md
|
|
fi
|
|
|
|
echo "" >> test-infrastructure-report.md
|
|
echo "---" >> test-infrastructure-report.md
|
|
echo "*Report generated at $(date -u +%Y-%m-%dT%H:%M:%SZ)*" >> test-infrastructure-report.md
|
|
|
|
cat test-infrastructure-report.md
|
|
cat test-infrastructure-report.md >> $GITHUB_STEP_SUMMARY
|
|
|
|
- name: Upload Report
|
|
uses: actions/upload-artifact@v4
|
|
with:
|
|
name: test-infrastructure-report
|
|
path: test-infrastructure-report.md
|
|
|
|
- name: Check for Failures
|
|
if: |
|
|
(needs.blast-radius.outputs.status == 'failed' ||
|
|
needs.dead-paths.outputs.status == 'failed' ||
|
|
needs.schema-evolution.outputs.status == 'failed' ||
|
|
needs.config-diff.outputs.status == 'failed') &&
|
|
inputs.fail_fast == true
|
|
run: |
|
|
echo "::error::One or more test infrastructure checks failed"
|
|
exit 1
|
|
|
|
# ===========================================================================
|
|
# POST PR COMMENT
|
|
# ===========================================================================
|
|
|
|
comment:
|
|
name: Post PR Comment
|
|
needs: [report, blast-radius, dead-paths, schema-evolution, config-diff]
|
|
if: github.event_name == 'pull_request' && always()
|
|
runs-on: ${{ vars.LINUX_RUNNER_LABEL || 'ubuntu-latest' }}
|
|
permissions:
|
|
pull-requests: write
|
|
steps:
|
|
- name: Download Report
|
|
uses: actions/download-artifact@v4
|
|
with:
|
|
name: test-infrastructure-report
|
|
continue-on-error: true
|
|
|
|
- name: Post Comment
|
|
uses: actions/github-script@v7
|
|
with:
|
|
script: |
|
|
const fs = require('fs');
|
|
let report = '';
|
|
try {
|
|
report = fs.readFileSync('test-infrastructure-report.md', 'utf8');
|
|
} catch (e) {
|
|
report = 'Test infrastructure report not available.';
|
|
}
|
|
|
|
// Check for any failures
|
|
const brStatus = '${{ needs.blast-radius.outputs.status }}';
|
|
const dpStatus = '${{ needs.dead-paths.outputs.status }}';
|
|
const seStatus = '${{ needs.schema-evolution.outputs.status }}';
|
|
const cdStatus = '${{ needs.config-diff.outputs.status }}';
|
|
|
|
const hasFailed = [brStatus, dpStatus, seStatus, cdStatus].includes('failed');
|
|
const allPassed = [brStatus, dpStatus, seStatus, cdStatus]
|
|
.filter(s => s !== 'skipped' && s !== '')
|
|
.every(s => s === 'passed' || s === 'completed');
|
|
|
|
let status;
|
|
if (hasFailed) {
|
|
status = ':x: Some checks failed';
|
|
} else if (allPassed) {
|
|
status = ':white_check_mark: All checks passed';
|
|
} else {
|
|
status = ':grey_question: Some checks skipped';
|
|
}
|
|
|
|
const body = `## Test Infrastructure ${status}
|
|
|
|
${report}
|
|
|
|
---
|
|
<details>
|
|
<summary>About Test Infrastructure Checks</summary>
|
|
|
|
This workflow validates cross-cutting testing standards:
|
|
|
|
- **Blast-Radius**: Ensures Integration/Contract/Security tests have BlastRadius annotations
|
|
- **Dead-Path Detection**: Identifies uncovered code branches
|
|
- **Schema Evolution**: Validates backward compatibility with previous schema versions
|
|
- **Config-Diff**: Ensures config changes produce only expected behavioral deltas
|
|
|
|
</details>
|
|
`;
|
|
|
|
// Find and update or create comment
|
|
const { data: comments } = await github.rest.issues.listComments({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
issue_number: context.issue.number
|
|
});
|
|
|
|
const botComment = comments.find(c =>
|
|
c.user.type === 'Bot' &&
|
|
c.body.includes('Test Infrastructure')
|
|
);
|
|
|
|
if (botComment) {
|
|
await github.rest.issues.updateComment({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
comment_id: botComment.id,
|
|
body: body
|
|
});
|
|
} else {
|
|
await github.rest.issues.createComment({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
issue_number: context.issue.number,
|
|
body: body
|
|
});
|
|
}
|
|
|