save progress
This commit is contained in:
418
.gitea/workflows/schema-evolution.yml
Normal file
418
.gitea/workflows/schema-evolution.yml
Normal file
@@ -0,0 +1,418 @@
|
||||
# .gitea/workflows/schema-evolution.yml
|
||||
# Schema evolution testing workflow for backward/forward compatibility
|
||||
# Sprint: SPRINT_20260105_002_005_TEST_cross_cutting
|
||||
# Task: CCUT-012
|
||||
#
|
||||
# WORKFLOW PURPOSE:
|
||||
# =================
|
||||
# Validates that code changes remain compatible with previous database schema
|
||||
# versions (N-1, N-2). This prevents breaking changes when new code is deployed
|
||||
# before database migrations complete, or when rollbacks occur.
|
||||
#
|
||||
# Uses Testcontainers with versioned PostgreSQL images to replay tests against
|
||||
# historical schema versions.
|
||||
|
||||
name: Schema Evolution Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'docs/db/**/*.sql'
|
||||
- 'src/**/Migrations/**'
|
||||
- 'src/**/*Repository*.cs'
|
||||
- 'src/**/*DbContext*.cs'
|
||||
- '.gitea/workflows/schema-evolution.yml'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'docs/db/**/*.sql'
|
||||
- 'src/**/Migrations/**'
|
||||
- 'src/**/*Repository*.cs'
|
||||
- 'src/**/*DbContext*.cs'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
schema_versions:
|
||||
description: 'Schema versions to test (comma-separated, e.g., N-1,N-2,N-3)'
|
||||
type: string
|
||||
default: 'N-1,N-2'
|
||||
modules:
|
||||
description: 'Modules to test (comma-separated, or "all")'
|
||||
type: string
|
||||
default: 'all'
|
||||
|
||||
env:
|
||||
DOTNET_VERSION: '10.0.100'
|
||||
DOTNET_NOLOGO: 1
|
||||
DOTNET_CLI_TELEMETRY_OPTOUT: 1
|
||||
SCHEMA_VERSIONS: 'N-1,N-2'
|
||||
|
||||
jobs:
|
||||
# ===========================================================================
|
||||
# DISCOVER SCHEMA-AFFECTED MODULES
|
||||
# ===========================================================================
|
||||
|
||||
discover:
|
||||
name: Discover Changed Modules
|
||||
runs-on: ubuntu-22.04
|
||||
outputs:
|
||||
modules: ${{ steps.detect.outputs.modules }}
|
||||
has-schema-changes: ${{ steps.detect.outputs.has_changes }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Detect Schema Changes
|
||||
id: detect
|
||||
run: |
|
||||
# Get changed files
|
||||
if [ "${{ github.event_name }}" = "pull_request" ]; then
|
||||
CHANGED_FILES=$(git diff --name-only ${{ github.event.pull_request.base.sha }} ${{ github.sha }})
|
||||
else
|
||||
CHANGED_FILES=$(git diff --name-only HEAD~1 HEAD)
|
||||
fi
|
||||
|
||||
echo "Changed files:"
|
||||
echo "$CHANGED_FILES"
|
||||
|
||||
# Map files to modules
|
||||
MODULES=""
|
||||
|
||||
if echo "$CHANGED_FILES" | grep -qE "src/Scanner/.*Repository|src/Scanner/.*Migrations|docs/db/.*scanner"; then
|
||||
MODULES="$MODULES,Scanner"
|
||||
fi
|
||||
|
||||
if echo "$CHANGED_FILES" | grep -qE "src/Concelier/.*Repository|src/Concelier/.*Migrations|docs/db/.*concelier|docs/db/.*advisory"; then
|
||||
MODULES="$MODULES,Concelier"
|
||||
fi
|
||||
|
||||
if echo "$CHANGED_FILES" | grep -qE "src/EvidenceLocker/.*Repository|src/EvidenceLocker/.*Migrations|docs/db/.*evidence"; then
|
||||
MODULES="$MODULES,EvidenceLocker"
|
||||
fi
|
||||
|
||||
if echo "$CHANGED_FILES" | grep -qE "src/Authority/.*Repository|src/Authority/.*Migrations|docs/db/.*authority|docs/db/.*auth"; then
|
||||
MODULES="$MODULES,Authority"
|
||||
fi
|
||||
|
||||
if echo "$CHANGED_FILES" | grep -qE "src/Policy/.*Repository|src/Policy/.*Migrations|docs/db/.*policy"; then
|
||||
MODULES="$MODULES,Policy"
|
||||
fi
|
||||
|
||||
if echo "$CHANGED_FILES" | grep -qE "src/SbomService/.*Repository|src/SbomService/.*Migrations|docs/db/.*sbom"; then
|
||||
MODULES="$MODULES,SbomService"
|
||||
fi
|
||||
|
||||
# Remove leading comma
|
||||
MODULES=$(echo "$MODULES" | sed 's/^,//')
|
||||
|
||||
if [ -z "$MODULES" ]; then
|
||||
echo "has_changes=false" >> $GITHUB_OUTPUT
|
||||
echo "modules=[]" >> $GITHUB_OUTPUT
|
||||
echo "No schema-related changes detected"
|
||||
else
|
||||
echo "has_changes=true" >> $GITHUB_OUTPUT
|
||||
# Convert to JSON array
|
||||
MODULES_JSON=$(echo "$MODULES" | tr ',' '\n' | jq -R . | jq -s .)
|
||||
echo "modules=$MODULES_JSON" >> $GITHUB_OUTPUT
|
||||
echo "Detected modules: $MODULES"
|
||||
fi
|
||||
|
||||
# ===========================================================================
|
||||
# RUN SCHEMA EVOLUTION TESTS
|
||||
# ===========================================================================
|
||||
|
||||
test:
|
||||
name: Test ${{ matrix.module }} (Schema ${{ matrix.schema-version }})
|
||||
needs: discover
|
||||
if: needs.discover.outputs.has-schema-changes == 'true' || github.event_name == 'workflow_dispatch'
|
||||
runs-on: ubuntu-22.04
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
module: ${{ fromJson(needs.discover.outputs.modules || '["Scanner","Concelier","EvidenceLocker"]') }}
|
||||
schema-version: ['N-1', 'N-2']
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
env:
|
||||
POSTGRES_USER: stellaops_test
|
||||
POSTGRES_PASSWORD: test_password
|
||||
POSTGRES_DB: stellaops_schema_test
|
||||
ports:
|
||||
- 5432:5432
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
env:
|
||||
STELLAOPS_TEST_POSTGRES_CONNECTION: "Host=localhost;Port=5432;Database=stellaops_schema_test;Username=stellaops_test;Password=test_password"
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: ${{ env.DOTNET_VERSION }}
|
||||
|
||||
- name: Cache NuGet packages
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.nuget/packages
|
||||
key: ${{ runner.os }}-nuget-${{ hashFiles('**/Directory.Packages.props', '**/*.csproj') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-nuget-
|
||||
|
||||
- name: Restore Dependencies
|
||||
run: dotnet restore src/StellaOps.sln
|
||||
|
||||
- name: Get Schema Version
|
||||
id: schema
|
||||
run: |
|
||||
# Get current schema version from migration history
|
||||
CURRENT_VERSION=$(ls -1 docs/db/migrations/${{ matrix.module }}/*.sql 2>/dev/null | wc -l || echo "1")
|
||||
|
||||
case "${{ matrix.schema-version }}" in
|
||||
"N-1")
|
||||
TARGET_VERSION=$((CURRENT_VERSION - 1))
|
||||
;;
|
||||
"N-2")
|
||||
TARGET_VERSION=$((CURRENT_VERSION - 2))
|
||||
;;
|
||||
"N-3")
|
||||
TARGET_VERSION=$((CURRENT_VERSION - 3))
|
||||
;;
|
||||
*)
|
||||
TARGET_VERSION=$CURRENT_VERSION
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ "$TARGET_VERSION" -lt 1 ]; then
|
||||
echo "skip=true" >> $GITHUB_OUTPUT
|
||||
echo "No previous schema version available for ${{ matrix.schema-version }}"
|
||||
else
|
||||
echo "skip=false" >> $GITHUB_OUTPUT
|
||||
echo "target_version=$TARGET_VERSION" >> $GITHUB_OUTPUT
|
||||
echo "Testing against schema version: $TARGET_VERSION"
|
||||
fi
|
||||
|
||||
- name: Apply Historical Schema
|
||||
if: steps.schema.outputs.skip != 'true'
|
||||
run: |
|
||||
# Apply schema up to target version
|
||||
TARGET=${{ steps.schema.outputs.target_version }}
|
||||
MODULE_LOWER=$(echo "${{ matrix.module }}" | tr '[:upper:]' '[:lower:]')
|
||||
|
||||
echo "Applying schema migrations up to version $TARGET for $MODULE_LOWER"
|
||||
|
||||
# Apply base schema
|
||||
if [ -f "docs/db/schemas/${MODULE_LOWER}.sql" ]; then
|
||||
psql "$STELLAOPS_TEST_POSTGRES_CONNECTION" -f "docs/db/schemas/${MODULE_LOWER}.sql" || true
|
||||
fi
|
||||
|
||||
# Apply migrations up to target version
|
||||
MIGRATION_COUNT=0
|
||||
for migration in $(ls -1 docs/db/migrations/${MODULE_LOWER}/*.sql 2>/dev/null | sort -V); do
|
||||
MIGRATION_COUNT=$((MIGRATION_COUNT + 1))
|
||||
if [ "$MIGRATION_COUNT" -le "$TARGET" ]; then
|
||||
echo "Applying: $migration"
|
||||
psql "$STELLAOPS_TEST_POSTGRES_CONNECTION" -f "$migration" || true
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Applied $MIGRATION_COUNT migrations"
|
||||
|
||||
- name: Run Schema Evolution Tests
|
||||
if: steps.schema.outputs.skip != 'true'
|
||||
id: test
|
||||
run: |
|
||||
# Find and run schema evolution tests for the module
|
||||
TEST_PROJECT="src/${{ matrix.module }}/__Tests/StellaOps.${{ matrix.module }}.SchemaEvolution.Tests"
|
||||
|
||||
if [ -d "$TEST_PROJECT" ]; then
|
||||
dotnet test "$TEST_PROJECT" \
|
||||
--configuration Release \
|
||||
--no-restore \
|
||||
--verbosity normal \
|
||||
--logger "trx;LogFileName=schema-evolution-${{ matrix.module }}-${{ matrix.schema-version }}.trx" \
|
||||
--results-directory ./test-results \
|
||||
-- RunConfiguration.EnvironmentVariables.SCHEMA_VERSION="${{ matrix.schema-version }}"
|
||||
else
|
||||
# Run tests with SchemaEvolution category from main test project
|
||||
TEST_PROJECT="src/${{ matrix.module }}/__Tests/StellaOps.${{ matrix.module }}.Tests"
|
||||
if [ -d "$TEST_PROJECT" ]; then
|
||||
dotnet test "$TEST_PROJECT" \
|
||||
--configuration Release \
|
||||
--no-restore \
|
||||
--verbosity normal \
|
||||
--filter "Category=SchemaEvolution" \
|
||||
--logger "trx;LogFileName=schema-evolution-${{ matrix.module }}-${{ matrix.schema-version }}.trx" \
|
||||
--results-directory ./test-results \
|
||||
-- RunConfiguration.EnvironmentVariables.SCHEMA_VERSION="${{ matrix.schema-version }}"
|
||||
else
|
||||
echo "No test project found for ${{ matrix.module }}"
|
||||
echo "skip_reason=no_tests" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
fi
|
||||
|
||||
- name: Upload Test Results
|
||||
if: always() && steps.schema.outputs.skip != 'true'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: schema-evolution-results-${{ matrix.module }}-${{ matrix.schema-version }}
|
||||
path: ./test-results/*.trx
|
||||
if-no-files-found: ignore
|
||||
|
||||
# ===========================================================================
|
||||
# COMPATIBILITY MATRIX REPORT
|
||||
# ===========================================================================
|
||||
|
||||
report:
|
||||
name: Generate Compatibility Report
|
||||
needs: [discover, test]
|
||||
if: always() && needs.discover.outputs.has-schema-changes == 'true'
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Download All Results
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
pattern: schema-evolution-results-*
|
||||
merge-multiple: true
|
||||
path: ./results
|
||||
continue-on-error: true
|
||||
|
||||
- name: Generate Report
|
||||
run: |
|
||||
cat > schema-compatibility-report.md << 'EOF'
|
||||
## Schema Evolution Compatibility Report
|
||||
|
||||
| Module | Schema N-1 | Schema N-2 |
|
||||
|--------|------------|------------|
|
||||
EOF
|
||||
|
||||
# Parse test results and generate matrix
|
||||
for module in Scanner Concelier EvidenceLocker Authority Policy SbomService; do
|
||||
N1_STATUS="-"
|
||||
N2_STATUS="-"
|
||||
|
||||
if [ -f "results/schema-evolution-${module}-N-1.trx" ]; then
|
||||
if grep -q 'outcome="Passed"' "results/schema-evolution-${module}-N-1.trx" 2>/dev/null; then
|
||||
N1_STATUS=":white_check_mark:"
|
||||
elif grep -q 'outcome="Failed"' "results/schema-evolution-${module}-N-1.trx" 2>/dev/null; then
|
||||
N1_STATUS=":x:"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -f "results/schema-evolution-${module}-N-2.trx" ]; then
|
||||
if grep -q 'outcome="Passed"' "results/schema-evolution-${module}-N-2.trx" 2>/dev/null; then
|
||||
N2_STATUS=":white_check_mark:"
|
||||
elif grep -q 'outcome="Failed"' "results/schema-evolution-${module}-N-2.trx" 2>/dev/null; then
|
||||
N2_STATUS=":x:"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "| $module | $N1_STATUS | $N2_STATUS |" >> schema-compatibility-report.md
|
||||
done
|
||||
|
||||
echo "" >> schema-compatibility-report.md
|
||||
echo "*Report generated at $(date -u +%Y-%m-%dT%H:%M:%SZ)*" >> schema-compatibility-report.md
|
||||
|
||||
cat schema-compatibility-report.md
|
||||
|
||||
- name: Upload Report
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: schema-compatibility-report
|
||||
path: schema-compatibility-report.md
|
||||
|
||||
# ===========================================================================
|
||||
# POST REPORT TO PR
|
||||
# ===========================================================================
|
||||
|
||||
comment:
|
||||
name: Post Report to PR
|
||||
needs: [discover, test, report]
|
||||
if: github.event_name == 'pull_request' && always()
|
||||
runs-on: ubuntu-22.04
|
||||
permissions:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Download Report
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: schema-compatibility-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('schema-compatibility-report.md', 'utf8');
|
||||
} catch (e) {
|
||||
report = 'Schema compatibility report not available.';
|
||||
}
|
||||
|
||||
const hasChanges = '${{ needs.discover.outputs.has-schema-changes }}' === 'true';
|
||||
|
||||
if (!hasChanges) {
|
||||
return; // No schema changes, no comment needed
|
||||
}
|
||||
|
||||
const body = `## Schema Evolution Test Results
|
||||
|
||||
This PR includes changes that may affect database compatibility.
|
||||
|
||||
${report}
|
||||
|
||||
---
|
||||
<details>
|
||||
<summary>About Schema Evolution Tests</summary>
|
||||
|
||||
Schema evolution tests verify that:
|
||||
- Current code works with previous schema versions (N-1, N-2)
|
||||
- Rolling deployments don't break during migration windows
|
||||
- Rollbacks are safe when schema hasn't been migrated yet
|
||||
|
||||
If tests fail, consider:
|
||||
1. Adding backward-compatible default values
|
||||
2. Using nullable columns for new fields
|
||||
3. Creating migration-safe queries
|
||||
4. Updating the compatibility matrix
|
||||
|
||||
</details>
|
||||
`;
|
||||
|
||||
// Find existing 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('Schema Evolution Test Results')
|
||||
);
|
||||
|
||||
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
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user