Fix build and code structure improvements. New but essential UI functionality. CI improvements. Documentation improvements. AI module improvements.

This commit is contained in:
StellaOps Bot
2025-12-26 21:54:17 +02:00
parent 335ff7da16
commit c2b9cd8d1f
3717 changed files with 264714 additions and 48202 deletions

View File

@@ -0,0 +1,227 @@
# Container Security Scanning Workflow
# Sprint: CI/CD Enhancement - Security Scanning
#
# Purpose: Scan container images for vulnerabilities beyond SBOM generation
# Triggers: Dockerfile changes, scheduled daily, manual dispatch
#
# Tool: PLACEHOLDER - Choose one: Trivy, Grype, or Snyk
name: Container Security Scan
on:
push:
paths:
- '**/Dockerfile'
- '**/Dockerfile.*'
- 'devops/docker/**'
pull_request:
paths:
- '**/Dockerfile'
- '**/Dockerfile.*'
- 'devops/docker/**'
schedule:
# Run daily at 4 AM UTC
- cron: '0 4 * * *'
workflow_dispatch:
inputs:
severity_threshold:
description: 'Minimum severity to fail'
required: false
type: choice
options:
- CRITICAL
- HIGH
- MEDIUM
- LOW
default: HIGH
image:
description: 'Specific image to scan (optional)'
required: false
type: string
env:
SEVERITY_THRESHOLD: ${{ github.event.inputs.severity_threshold || 'HIGH' }}
jobs:
discover-images:
name: Discover Container Images
runs-on: ubuntu-latest
outputs:
images: ${{ steps.discover.outputs.images }}
count: ${{ steps.discover.outputs.count }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Discover Dockerfiles
id: discover
run: |
# Find all Dockerfiles
DOCKERFILES=$(find . -name "Dockerfile" -o -name "Dockerfile.*" | grep -v node_modules | grep -v bin | grep -v obj || true)
# Build image list
IMAGES='[]'
COUNT=0
while IFS= read -r dockerfile; do
if [[ -n "$dockerfile" ]]; then
DIR=$(dirname "$dockerfile")
NAME=$(basename "$DIR" | tr '[:upper:]' '[:lower:]' | tr '.' '-')
# Get image name from directory structure
if [[ "$DIR" == *"devops/docker"* ]]; then
NAME=$(echo "$dockerfile" | sed 's|.*devops/docker/||' | sed 's|/Dockerfile.*||' | tr '/' '-')
fi
IMAGES=$(echo "$IMAGES" | jq --arg name "$NAME" --arg path "$dockerfile" '. + [{"name": $name, "dockerfile": $path}]')
COUNT=$((COUNT + 1))
fi
done <<< "$DOCKERFILES"
echo "Found $COUNT Dockerfile(s)"
echo "images=$(echo "$IMAGES" | jq -c .)" >> $GITHUB_OUTPUT
echo "count=$COUNT" >> $GITHUB_OUTPUT
scan-images:
name: Scan ${{ matrix.image.name }}
runs-on: ubuntu-latest
needs: [discover-images]
if: needs.discover-images.outputs.count != '0'
strategy:
fail-fast: false
matrix:
image: ${{ fromJson(needs.discover-images.outputs.images) }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build image for scanning
id: build
run: |
IMAGE_TAG="scan-${{ matrix.image.name }}:${{ github.sha }}"
DOCKERFILE="${{ matrix.image.dockerfile }}"
CONTEXT=$(dirname "$DOCKERFILE")
echo "Building $IMAGE_TAG from $DOCKERFILE..."
docker build -t "$IMAGE_TAG" -f "$DOCKERFILE" "$CONTEXT" || {
echo "::warning::Failed to build $IMAGE_TAG - skipping scan"
echo "skip=true" >> $GITHUB_OUTPUT
exit 0
}
echo "image_tag=$IMAGE_TAG" >> $GITHUB_OUTPUT
echo "skip=false" >> $GITHUB_OUTPUT
# PLACEHOLDER: Choose your container scanner
# Option 1: Trivy (recommended - comprehensive, free)
# Option 2: Grype (Anchore - good integration with Syft SBOMs)
# Option 3: Snyk (commercial, comprehensive)
- name: Trivy Vulnerability Scan
if: steps.build.outputs.skip != 'true'
id: trivy
# Uncomment when ready to use Trivy:
# uses: aquasecurity/trivy-action@master
# with:
# image-ref: ${{ steps.build.outputs.image_tag }}
# format: 'sarif'
# output: 'trivy-${{ matrix.image.name }}.sarif'
# severity: ${{ env.SEVERITY_THRESHOLD }},CRITICAL
# exit-code: '1'
run: |
echo "::notice::Container scanning placeholder - configure scanner below"
echo ""
echo "Image: ${{ steps.build.outputs.image_tag }}"
echo "Severity threshold: ${{ env.SEVERITY_THRESHOLD }}"
echo ""
echo "Available scanners:"
echo " 1. Trivy: aquasecurity/trivy-action@master"
echo " 2. Grype: anchore/scan-action@v3"
echo " 3. Snyk: snyk/actions/docker@master"
# Create placeholder report
mkdir -p scan-results
echo '{"placeholder": true, "image": "${{ matrix.image.name }}"}' > scan-results/scan-${{ matrix.image.name }}.json
# Alternative: Grype (works well with existing Syft SBOM workflow)
# - name: Grype Vulnerability Scan
# if: steps.build.outputs.skip != 'true'
# uses: anchore/scan-action@v3
# with:
# image: ${{ steps.build.outputs.image_tag }}
# severity-cutoff: ${{ env.SEVERITY_THRESHOLD }}
# fail-build: true
# Alternative: Snyk Container
# - name: Snyk Container Scan
# if: steps.build.outputs.skip != 'true'
# uses: snyk/actions/docker@master
# env:
# SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
# with:
# image: ${{ steps.build.outputs.image_tag }}
# args: --severity-threshold=${{ env.SEVERITY_THRESHOLD }}
- name: Upload scan results
if: always() && steps.build.outputs.skip != 'true'
uses: actions/upload-artifact@v4
with:
name: container-scan-${{ matrix.image.name }}
path: |
scan-results/
*.sarif
*.json
retention-days: 30
if-no-files-found: ignore
- name: Cleanup
if: always()
run: |
docker rmi "${{ steps.build.outputs.image_tag }}" 2>/dev/null || true
summary:
name: Scan Summary
runs-on: ubuntu-latest
needs: [discover-images, scan-images]
if: always()
steps:
- name: Download all scan results
uses: actions/download-artifact@v4
with:
pattern: container-scan-*
path: all-results/
merge-multiple: true
continue-on-error: true
- name: Generate summary
run: |
echo "## Container Security Scan Results" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Image | Status |" >> $GITHUB_STEP_SUMMARY
echo "|-------|--------|" >> $GITHUB_STEP_SUMMARY
IMAGES='${{ needs.discover-images.outputs.images }}'
SCAN_RESULT="${{ needs.scan-images.result }}"
echo "$IMAGES" | jq -r '.[] | .name' | while read -r name; do
if [[ "$SCAN_RESULT" == "success" ]]; then
echo "| $name | No vulnerabilities found |" >> $GITHUB_STEP_SUMMARY
elif [[ "$SCAN_RESULT" == "failure" ]]; then
echo "| $name | Vulnerabilities detected |" >> $GITHUB_STEP_SUMMARY
else
echo "| $name | $SCAN_RESULT |" >> $GITHUB_STEP_SUMMARY
fi
done
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Configuration" >> $GITHUB_STEP_SUMMARY
echo "- **Scanner:** Placeholder (configure in workflow)" >> $GITHUB_STEP_SUMMARY
echo "- **Severity Threshold:** ${{ env.SEVERITY_THRESHOLD }}" >> $GITHUB_STEP_SUMMARY
echo "- **Images Scanned:** ${{ needs.discover-images.outputs.count }}" >> $GITHUB_STEP_SUMMARY
echo "- **Trigger:** ${{ github.event_name }}" >> $GITHUB_STEP_SUMMARY

View File

@@ -0,0 +1,204 @@
# Dependency License Compliance Gate
# Sprint: CI/CD Enhancement - Dependency Management Automation
#
# Purpose: Validate that all dependencies use approved licenses
# Triggers: PRs modifying package files
name: License Compliance
on:
pull_request:
paths:
- 'src/Directory.Packages.props'
- '**/package.json'
- '**/package-lock.json'
- '**/*.csproj'
env:
DOTNET_VERSION: '10.0.100'
# Blocked licenses (incompatible with AGPL-3.0)
BLOCKED_LICENSES: 'GPL-2.0-only,SSPL-1.0,BUSL-1.1,Proprietary,Commercial'
# Allowed licenses
ALLOWED_LICENSES: 'MIT,Apache-2.0,BSD-2-Clause,BSD-3-Clause,ISC,0BSD,Unlicense,CC0-1.0,LGPL-2.1,LGPL-3.0,MPL-2.0,AGPL-3.0,GPL-3.0'
jobs:
check-nuget-licenses:
name: NuGet License Check
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: ${{ env.DOTNET_VERSION }}
include-prerelease: true
- name: Install dotnet-delice
run: dotnet tool install --global dotnet-delice
- name: Restore packages
run: dotnet restore src/StellaOps.sln
- name: Check NuGet licenses
id: nuget-check
run: |
mkdir -p license-reports
echo "Checking NuGet package licenses..."
# Run delice on the solution
dotnet delice src/StellaOps.sln \
--output license-reports/nuget-licenses.json \
--format json \
2>&1 | tee license-reports/nuget-check.log || true
# Check for blocked licenses
BLOCKED_FOUND=0
BLOCKED_PACKAGES=""
IFS=',' read -ra BLOCKED_ARRAY <<< "$BLOCKED_LICENSES"
for license in "${BLOCKED_ARRAY[@]}"; do
if grep -qi "\"$license\"" license-reports/nuget-licenses.json 2>/dev/null; then
BLOCKED_FOUND=1
PACKAGES=$(grep -B5 "\"$license\"" license-reports/nuget-licenses.json | grep -o '"[^"]*"' | head -1 || echo "unknown")
BLOCKED_PACKAGES="$BLOCKED_PACKAGES\n- $license: $PACKAGES"
fi
done
if [[ $BLOCKED_FOUND -eq 1 ]]; then
echo "::error::Blocked licenses found in NuGet packages:$BLOCKED_PACKAGES"
echo "blocked=true" >> $GITHUB_OUTPUT
echo "blocked_packages<<EOF" >> $GITHUB_OUTPUT
echo -e "$BLOCKED_PACKAGES" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
else
echo "All NuGet packages have approved licenses"
echo "blocked=false" >> $GITHUB_OUTPUT
fi
- name: Upload NuGet license report
uses: actions/upload-artifact@v4
with:
name: nuget-license-report
path: license-reports/
retention-days: 30
check-npm-licenses:
name: npm License Check
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Find package.json files
id: find-packages
run: |
PACKAGES=$(find . -name "package.json" -not -path "*/node_modules/*" -not -path "*/bin/*" -not -path "*/obj/*" | head -10)
echo "Found package.json files:"
echo "$PACKAGES"
echo "packages<<EOF" >> $GITHUB_OUTPUT
echo "$PACKAGES" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
- name: Install license-checker
run: npm install -g license-checker
- name: Check npm licenses
id: npm-check
run: |
mkdir -p license-reports
BLOCKED_FOUND=0
BLOCKED_PACKAGES=""
# Check each package.json directory
while IFS= read -r pkg; do
if [[ -z "$pkg" ]]; then continue; fi
DIR=$(dirname "$pkg")
echo "Checking $DIR..."
cd "$DIR"
if [[ -f "package-lock.json" ]] || [[ -f "yarn.lock" ]]; then
npm install --ignore-scripts 2>/dev/null || true
# Run license checker
license-checker --json > "${GITHUB_WORKSPACE}/license-reports/npm-$(basename $DIR).json" 2>/dev/null || true
# Check for blocked licenses
IFS=',' read -ra BLOCKED_ARRAY <<< "$BLOCKED_LICENSES"
for license in "${BLOCKED_ARRAY[@]}"; do
if grep -qi "\"$license\"" "${GITHUB_WORKSPACE}/license-reports/npm-$(basename $DIR).json" 2>/dev/null; then
BLOCKED_FOUND=1
BLOCKED_PACKAGES="$BLOCKED_PACKAGES\n- $license in $DIR"
fi
done
fi
cd "$GITHUB_WORKSPACE"
done <<< "${{ steps.find-packages.outputs.packages }}"
if [[ $BLOCKED_FOUND -eq 1 ]]; then
echo "::error::Blocked licenses found in npm packages:$BLOCKED_PACKAGES"
echo "blocked=true" >> $GITHUB_OUTPUT
else
echo "All npm packages have approved licenses"
echo "blocked=false" >> $GITHUB_OUTPUT
fi
- name: Upload npm license report
uses: actions/upload-artifact@v4
if: always()
with:
name: npm-license-report
path: license-reports/
retention-days: 30
gate:
name: License Gate
runs-on: ubuntu-latest
needs: [check-nuget-licenses, check-npm-licenses]
if: always()
steps:
- name: Check results
run: |
NUGET_BLOCKED="${{ needs.check-nuget-licenses.outputs.blocked }}"
NPM_BLOCKED="${{ needs.check-npm-licenses.outputs.blocked }}"
echo "## License Compliance Results" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Check | Status |" >> $GITHUB_STEP_SUMMARY
echo "|-------|--------|" >> $GITHUB_STEP_SUMMARY
if [[ "$NUGET_BLOCKED" == "true" ]]; then
echo "| NuGet | ❌ Blocked licenses found |" >> $GITHUB_STEP_SUMMARY
else
echo "| NuGet | ✅ Approved |" >> $GITHUB_STEP_SUMMARY
fi
if [[ "$NPM_BLOCKED" == "true" ]]; then
echo "| npm | ❌ Blocked licenses found |" >> $GITHUB_STEP_SUMMARY
else
echo "| npm | ✅ Approved |" >> $GITHUB_STEP_SUMMARY
fi
if [[ "$NUGET_BLOCKED" == "true" ]] || [[ "$NPM_BLOCKED" == "true" ]]; then
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Blocked Licenses" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "The following licenses are not compatible with AGPL-3.0:" >> $GITHUB_STEP_SUMMARY
echo "\`$BLOCKED_LICENSES\`" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "Please replace the offending packages or request an exception." >> $GITHUB_STEP_SUMMARY
echo "::error::License compliance check failed"
exit 1
fi
echo "" >> $GITHUB_STEP_SUMMARY
echo "✅ All dependencies use approved licenses" >> $GITHUB_STEP_SUMMARY

View File

@@ -0,0 +1,249 @@
# Dependency Security Scan
# Sprint: CI/CD Enhancement - Dependency Management Automation
#
# Purpose: Scan dependencies for known vulnerabilities
# Schedule: Weekly and on PRs modifying package files
name: Dependency Security Scan
on:
schedule:
# Run weekly on Sundays at 02:00 UTC
- cron: '0 2 * * 0'
pull_request:
paths:
- 'src/Directory.Packages.props'
- '**/package.json'
- '**/package-lock.json'
- '**/*.csproj'
workflow_dispatch:
inputs:
fail_on_vulnerabilities:
description: 'Fail if vulnerabilities found'
required: false
type: boolean
default: true
env:
DOTNET_VERSION: '10.0.100'
jobs:
scan-nuget:
name: NuGet Vulnerability Scan
runs-on: ubuntu-latest
outputs:
vulnerabilities_found: ${{ steps.scan.outputs.vulnerabilities_found }}
critical_count: ${{ steps.scan.outputs.critical_count }}
high_count: ${{ steps.scan.outputs.high_count }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: ${{ env.DOTNET_VERSION }}
include-prerelease: true
- name: Restore packages
run: dotnet restore src/StellaOps.sln
- name: Scan for vulnerabilities
id: scan
run: |
mkdir -p security-reports
echo "Scanning NuGet packages for vulnerabilities..."
# Run vulnerability check
dotnet list src/StellaOps.sln package --vulnerable --include-transitive \
> security-reports/nuget-vulnerabilities.txt 2>&1 || true
# Parse results
CRITICAL=$(grep -c "Critical" security-reports/nuget-vulnerabilities.txt 2>/dev/null || echo "0")
HIGH=$(grep -c "High" security-reports/nuget-vulnerabilities.txt 2>/dev/null || echo "0")
MEDIUM=$(grep -c "Medium" security-reports/nuget-vulnerabilities.txt 2>/dev/null || echo "0")
LOW=$(grep -c "Low" security-reports/nuget-vulnerabilities.txt 2>/dev/null || echo "0")
TOTAL=$((CRITICAL + HIGH + MEDIUM + LOW))
echo "=== Vulnerability Summary ==="
echo "Critical: $CRITICAL"
echo "High: $HIGH"
echo "Medium: $MEDIUM"
echo "Low: $LOW"
echo "Total: $TOTAL"
echo "critical_count=$CRITICAL" >> $GITHUB_OUTPUT
echo "high_count=$HIGH" >> $GITHUB_OUTPUT
echo "medium_count=$MEDIUM" >> $GITHUB_OUTPUT
echo "low_count=$LOW" >> $GITHUB_OUTPUT
if [[ $TOTAL -gt 0 ]]; then
echo "vulnerabilities_found=true" >> $GITHUB_OUTPUT
else
echo "vulnerabilities_found=false" >> $GITHUB_OUTPUT
fi
# Show detailed report
echo ""
echo "=== Detailed Report ==="
cat security-reports/nuget-vulnerabilities.txt
- name: Upload NuGet security report
uses: actions/upload-artifact@v4
with:
name: nuget-security-report
path: security-reports/
retention-days: 90
scan-npm:
name: npm Vulnerability Scan
runs-on: ubuntu-latest
outputs:
vulnerabilities_found: ${{ steps.scan.outputs.vulnerabilities_found }}
critical_count: ${{ steps.scan.outputs.critical_count }}
high_count: ${{ steps.scan.outputs.high_count }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Find and scan package.json files
id: scan
run: |
mkdir -p security-reports
TOTAL_CRITICAL=0
TOTAL_HIGH=0
TOTAL_MEDIUM=0
TOTAL_LOW=0
VULNERABILITIES_FOUND=false
# Find all package.json files
PACKAGES=$(find . -name "package.json" -not -path "*/node_modules/*" -not -path "*/bin/*" -not -path "*/obj/*")
for pkg in $PACKAGES; do
DIR=$(dirname "$pkg")
if [[ ! -f "$DIR/package-lock.json" ]] && [[ ! -f "$DIR/yarn.lock" ]]; then
continue
fi
echo "Scanning $DIR..."
cd "$DIR"
# Install dependencies
npm install --ignore-scripts 2>/dev/null || true
# Run npm audit
REPORT_FILE="${GITHUB_WORKSPACE}/security-reports/npm-audit-$(basename $DIR).json"
npm audit --json > "$REPORT_FILE" 2>/dev/null || true
# Parse results
if [[ -f "$REPORT_FILE" ]]; then
CRITICAL=$(jq '.metadata.vulnerabilities.critical // 0' "$REPORT_FILE" 2>/dev/null || echo "0")
HIGH=$(jq '.metadata.vulnerabilities.high // 0' "$REPORT_FILE" 2>/dev/null || echo "0")
MEDIUM=$(jq '.metadata.vulnerabilities.moderate // 0' "$REPORT_FILE" 2>/dev/null || echo "0")
LOW=$(jq '.metadata.vulnerabilities.low // 0' "$REPORT_FILE" 2>/dev/null || echo "0")
TOTAL_CRITICAL=$((TOTAL_CRITICAL + CRITICAL))
TOTAL_HIGH=$((TOTAL_HIGH + HIGH))
TOTAL_MEDIUM=$((TOTAL_MEDIUM + MEDIUM))
TOTAL_LOW=$((TOTAL_LOW + LOW))
if [[ $((CRITICAL + HIGH + MEDIUM + LOW)) -gt 0 ]]; then
VULNERABILITIES_FOUND=true
fi
fi
cd "$GITHUB_WORKSPACE"
done
echo "=== npm Vulnerability Summary ==="
echo "Critical: $TOTAL_CRITICAL"
echo "High: $TOTAL_HIGH"
echo "Medium: $TOTAL_MEDIUM"
echo "Low: $TOTAL_LOW"
echo "critical_count=$TOTAL_CRITICAL" >> $GITHUB_OUTPUT
echo "high_count=$TOTAL_HIGH" >> $GITHUB_OUTPUT
echo "vulnerabilities_found=$VULNERABILITIES_FOUND" >> $GITHUB_OUTPUT
- name: Upload npm security report
uses: actions/upload-artifact@v4
with:
name: npm-security-report
path: security-reports/
retention-days: 90
summary:
name: Security Summary
runs-on: ubuntu-latest
needs: [scan-nuget, scan-npm]
if: always()
steps:
- name: Generate summary
run: |
NUGET_VULNS="${{ needs.scan-nuget.outputs.vulnerabilities_found }}"
NPM_VULNS="${{ needs.scan-npm.outputs.vulnerabilities_found }}"
NUGET_CRITICAL="${{ needs.scan-nuget.outputs.critical_count }}"
NUGET_HIGH="${{ needs.scan-nuget.outputs.high_count }}"
NPM_CRITICAL="${{ needs.scan-npm.outputs.critical_count }}"
NPM_HIGH="${{ needs.scan-npm.outputs.high_count }}"
echo "## Dependency Security Scan Results" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### NuGet Packages" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Severity | Count |" >> $GITHUB_STEP_SUMMARY
echo "|----------|-------|" >> $GITHUB_STEP_SUMMARY
echo "| Critical | ${NUGET_CRITICAL:-0} |" >> $GITHUB_STEP_SUMMARY
echo "| High | ${NUGET_HIGH:-0} |" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### npm Packages" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Severity | Count |" >> $GITHUB_STEP_SUMMARY
echo "|----------|-------|" >> $GITHUB_STEP_SUMMARY
echo "| Critical | ${NPM_CRITICAL:-0} |" >> $GITHUB_STEP_SUMMARY
echo "| High | ${NPM_HIGH:-0} |" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
# Determine overall status
TOTAL_CRITICAL=$((${NUGET_CRITICAL:-0} + ${NPM_CRITICAL:-0}))
TOTAL_HIGH=$((${NUGET_HIGH:-0} + ${NPM_HIGH:-0}))
if [[ $TOTAL_CRITICAL -gt 0 ]]; then
echo "### ⚠️ Critical Vulnerabilities Found" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "Please review and remediate critical vulnerabilities before merging." >> $GITHUB_STEP_SUMMARY
elif [[ $TOTAL_HIGH -gt 0 ]]; then
echo "### ⚠️ High Severity Vulnerabilities Found" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "Please review high severity vulnerabilities." >> $GITHUB_STEP_SUMMARY
else
echo "### ✅ No Critical or High Vulnerabilities" >> $GITHUB_STEP_SUMMARY
fi
- name: Check gate
if: github.event.inputs.fail_on_vulnerabilities == 'true' || github.event_name == 'pull_request'
run: |
NUGET_CRITICAL="${{ needs.scan-nuget.outputs.critical_count }}"
NPM_CRITICAL="${{ needs.scan-npm.outputs.critical_count }}"
TOTAL_CRITICAL=$((${NUGET_CRITICAL:-0} + ${NPM_CRITICAL:-0}))
if [[ $TOTAL_CRITICAL -gt 0 ]]; then
echo "::error::$TOTAL_CRITICAL critical vulnerabilities found in dependencies"
exit 1
fi
echo "Security scan passed - no critical vulnerabilities"

View File

@@ -0,0 +1,512 @@
# .gitea/workflows/migration-test.yml
# Database Migration Testing Workflow
# Sprint: CI/CD Enhancement - Migration Safety
#
# Purpose: Validate database migrations work correctly in both directions
# - Forward migrations (upgrade)
# - Backward migrations (rollback)
# - Idempotency checks (re-running migrations)
# - Data integrity verification
#
# Triggers:
# - Pull requests that modify migration files
# - Scheduled daily validation
# - Manual dispatch for full migration suite
#
# Prerequisites:
# - PostgreSQL 16+ database
# - EF Core migrations in src/**/Migrations/
# - Migration scripts in devops/database/migrations/
name: Migration Testing
on:
push:
branches: [main]
paths:
- '**/Migrations/**'
- 'devops/database/**'
pull_request:
paths:
- '**/Migrations/**'
- 'devops/database/**'
schedule:
- cron: '30 4 * * *' # Daily at 4:30 AM UTC
workflow_dispatch:
inputs:
test_rollback:
description: 'Test rollback migrations'
type: boolean
default: true
test_idempotency:
description: 'Test migration idempotency'
type: boolean
default: true
target_module:
description: 'Specific module to test (empty = all)'
type: string
default: ''
baseline_version:
description: 'Baseline version to test from'
type: string
default: ''
env:
DOTNET_VERSION: '10.0.100'
DOTNET_NOLOGO: 1
DOTNET_CLI_TELEMETRY_OPTOUT: 1
TZ: UTC
POSTGRES_HOST: localhost
POSTGRES_PORT: 5432
POSTGRES_USER: stellaops_migration
POSTGRES_PASSWORD: migration_test_password
POSTGRES_DB: stellaops_migration_test
jobs:
# ===========================================================================
# DISCOVER MODULES WITH MIGRATIONS
# ===========================================================================
discover:
name: Discover Migrations
runs-on: ubuntu-22.04
outputs:
modules: ${{ steps.find.outputs.modules }}
module_count: ${{ steps.find.outputs.count }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Find modules with migrations
id: find
run: |
# Find all EF Core migration directories
MODULES=$(find src -type d -name "Migrations" -path "*/Persistence/*" | \
sed 's|/Migrations||' | \
sort -u | \
jq -R -s -c 'split("\n") | map(select(length > 0))')
COUNT=$(echo "$MODULES" | jq 'length')
echo "Found $COUNT modules with migrations"
echo "$MODULES" | jq -r '.[]'
# Filter by target module if specified
if [[ -n "${{ github.event.inputs.target_module }}" ]]; then
MODULES=$(echo "$MODULES" | jq -c --arg target "${{ github.event.inputs.target_module }}" \
'map(select(contains($target)))')
COUNT=$(echo "$MODULES" | jq 'length')
echo "Filtered to $COUNT modules matching: ${{ github.event.inputs.target_module }}"
fi
echo "modules=$MODULES" >> $GITHUB_OUTPUT
echo "count=$COUNT" >> $GITHUB_OUTPUT
- name: Display discovered modules
run: |
echo "## Discovered Migration Modules" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Module | Path |" >> $GITHUB_STEP_SUMMARY
echo "|--------|------|" >> $GITHUB_STEP_SUMMARY
for path in $(echo '${{ steps.find.outputs.modules }}' | jq -r '.[]'); do
module=$(basename $(dirname "$path"))
echo "| $module | $path |" >> $GITHUB_STEP_SUMMARY
done
# ===========================================================================
# FORWARD MIGRATION TESTS
# ===========================================================================
forward-migrations:
name: Forward Migration
runs-on: ubuntu-22.04
timeout-minutes: 30
needs: discover
if: needs.discover.outputs.module_count != '0'
services:
postgres:
image: postgres:16
env:
POSTGRES_USER: ${{ env.POSTGRES_USER }}
POSTGRES_PASSWORD: ${{ env.POSTGRES_PASSWORD }}
POSTGRES_DB: ${{ env.POSTGRES_DB }}
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
strategy:
fail-fast: false
matrix:
module: ${{ fromJson(needs.discover.outputs.modules) }}
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 }}
include-prerelease: true
- name: Install EF Core tools
run: dotnet tool install -g dotnet-ef
- name: Get module name
id: module
run: |
MODULE_NAME=$(basename $(dirname "${{ matrix.module }}"))
echo "name=$MODULE_NAME" >> $GITHUB_OUTPUT
echo "Testing module: $MODULE_NAME"
- name: Find project file
id: project
run: |
# Find the csproj file in the persistence directory
PROJECT_FILE=$(find "${{ matrix.module }}" -maxdepth 1 -name "*.csproj" | head -1)
if [[ -z "$PROJECT_FILE" ]]; then
echo "::error::No project file found in ${{ matrix.module }}"
exit 1
fi
echo "project=$PROJECT_FILE" >> $GITHUB_OUTPUT
echo "Found project: $PROJECT_FILE"
- name: Create fresh database
run: |
PGPASSWORD=${{ env.POSTGRES_PASSWORD }} psql -h ${{ env.POSTGRES_HOST }} \
-U ${{ env.POSTGRES_USER }} -d postgres \
-c "DROP DATABASE IF EXISTS ${{ env.POSTGRES_DB }}_${{ steps.module.outputs.name }};"
PGPASSWORD=${{ env.POSTGRES_PASSWORD }} psql -h ${{ env.POSTGRES_HOST }} \
-U ${{ env.POSTGRES_USER }} -d postgres \
-c "CREATE DATABASE ${{ env.POSTGRES_DB }}_${{ steps.module.outputs.name }};"
- name: Apply all migrations (forward)
id: forward
env:
ConnectionStrings__Default: "Host=${{ env.POSTGRES_HOST }};Port=${{ env.POSTGRES_PORT }};Database=${{ env.POSTGRES_DB }}_${{ steps.module.outputs.name }};Username=${{ env.POSTGRES_USER }};Password=${{ env.POSTGRES_PASSWORD }}"
run: |
echo "Applying migrations for ${{ steps.module.outputs.name }}..."
# List available migrations first
dotnet ef migrations list --project "${{ steps.project.outputs.project }}" \
--no-build 2>/dev/null || true
# Apply all migrations
START_TIME=$(date +%s)
dotnet ef database update --project "${{ steps.project.outputs.project }}"
END_TIME=$(date +%s)
DURATION=$((END_TIME - START_TIME))
echo "duration=$DURATION" >> $GITHUB_OUTPUT
echo "Migration completed in ${DURATION}s"
- name: Verify schema
env:
PGPASSWORD: ${{ env.POSTGRES_PASSWORD }}
run: |
echo "## Schema verification for ${{ steps.module.outputs.name }}" >> $GITHUB_STEP_SUMMARY
# Get table count
TABLE_COUNT=$(psql -h ${{ env.POSTGRES_HOST }} -U ${{ env.POSTGRES_USER }} \
-d "${{ env.POSTGRES_DB }}_${{ steps.module.outputs.name }}" -t -c \
"SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = 'public';")
echo "- Tables created: $TABLE_COUNT" >> $GITHUB_STEP_SUMMARY
echo "- Migration time: ${{ steps.forward.outputs.duration }}s" >> $GITHUB_STEP_SUMMARY
# List tables
echo "" >> $GITHUB_STEP_SUMMARY
echo "<details><summary>Tables</summary>" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
psql -h ${{ env.POSTGRES_HOST }} -U ${{ env.POSTGRES_USER }} \
-d "${{ env.POSTGRES_DB }}_${{ steps.module.outputs.name }}" -c \
"SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' ORDER BY table_name;" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
echo "</details>" >> $GITHUB_STEP_SUMMARY
- name: Upload migration log
uses: actions/upload-artifact@v4
if: always()
with:
name: migration-forward-${{ steps.module.outputs.name }}
path: |
**/*.migration.log
retention-days: 7
# ===========================================================================
# ROLLBACK MIGRATION TESTS
# ===========================================================================
rollback-migrations:
name: Rollback Migration
runs-on: ubuntu-22.04
timeout-minutes: 30
needs: [discover, forward-migrations]
if: |
needs.discover.outputs.module_count != '0' &&
(github.event_name == 'schedule' || github.event.inputs.test_rollback == 'true')
services:
postgres:
image: postgres:16
env:
POSTGRES_USER: ${{ env.POSTGRES_USER }}
POSTGRES_PASSWORD: ${{ env.POSTGRES_PASSWORD }}
POSTGRES_DB: ${{ env.POSTGRES_DB }}
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
strategy:
fail-fast: false
matrix:
module: ${{ fromJson(needs.discover.outputs.modules) }}
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 }}
include-prerelease: true
- name: Install EF Core tools
run: dotnet tool install -g dotnet-ef
- name: Get module info
id: module
run: |
MODULE_NAME=$(basename $(dirname "${{ matrix.module }}"))
echo "name=$MODULE_NAME" >> $GITHUB_OUTPUT
PROJECT_FILE=$(find "${{ matrix.module }}" -maxdepth 1 -name "*.csproj" | head -1)
echo "project=$PROJECT_FILE" >> $GITHUB_OUTPUT
- name: Create and migrate database
env:
ConnectionStrings__Default: "Host=${{ env.POSTGRES_HOST }};Port=${{ env.POSTGRES_PORT }};Database=${{ env.POSTGRES_DB }}_rb_${{ steps.module.outputs.name }};Username=${{ env.POSTGRES_USER }};Password=${{ env.POSTGRES_PASSWORD }}"
PGPASSWORD: ${{ env.POSTGRES_PASSWORD }}
run: |
# Create database
psql -h ${{ env.POSTGRES_HOST }} -U ${{ env.POSTGRES_USER }} -d postgres \
-c "DROP DATABASE IF EXISTS ${{ env.POSTGRES_DB }}_rb_${{ steps.module.outputs.name }};"
psql -h ${{ env.POSTGRES_HOST }} -U ${{ env.POSTGRES_USER }} -d postgres \
-c "CREATE DATABASE ${{ env.POSTGRES_DB }}_rb_${{ steps.module.outputs.name }};"
# Apply all migrations
dotnet ef database update --project "${{ steps.module.outputs.project }}"
- name: Get migration list
id: migrations
env:
ConnectionStrings__Default: "Host=${{ env.POSTGRES_HOST }};Port=${{ env.POSTGRES_PORT }};Database=${{ env.POSTGRES_DB }}_rb_${{ steps.module.outputs.name }};Username=${{ env.POSTGRES_USER }};Password=${{ env.POSTGRES_PASSWORD }}"
run: |
# Get list of applied migrations
MIGRATIONS=$(dotnet ef migrations list --project "${{ steps.module.outputs.project }}" \
--no-build 2>/dev/null | grep -E "^\d{14}_" | tail -5)
MIGRATION_COUNT=$(echo "$MIGRATIONS" | wc -l)
echo "count=$MIGRATION_COUNT" >> $GITHUB_OUTPUT
if [[ $MIGRATION_COUNT -gt 1 ]]; then
# Get the second-to-last migration for rollback target
ROLLBACK_TARGET=$(echo "$MIGRATIONS" | tail -2 | head -1)
echo "rollback_to=$ROLLBACK_TARGET" >> $GITHUB_OUTPUT
echo "Will rollback to: $ROLLBACK_TARGET"
else
echo "rollback_to=" >> $GITHUB_OUTPUT
echo "Not enough migrations to test rollback"
fi
- name: Test rollback
if: steps.migrations.outputs.rollback_to != ''
env:
ConnectionStrings__Default: "Host=${{ env.POSTGRES_HOST }};Port=${{ env.POSTGRES_PORT }};Database=${{ env.POSTGRES_DB }}_rb_${{ steps.module.outputs.name }};Username=${{ env.POSTGRES_USER }};Password=${{ env.POSTGRES_PASSWORD }}"
run: |
echo "Rolling back to: ${{ steps.migrations.outputs.rollback_to }}"
dotnet ef database update "${{ steps.migrations.outputs.rollback_to }}" \
--project "${{ steps.module.outputs.project }}"
echo "Rollback successful!"
- name: Test re-apply after rollback
if: steps.migrations.outputs.rollback_to != ''
env:
ConnectionStrings__Default: "Host=${{ env.POSTGRES_HOST }};Port=${{ env.POSTGRES_PORT }};Database=${{ env.POSTGRES_DB }}_rb_${{ steps.module.outputs.name }};Username=${{ env.POSTGRES_USER }};Password=${{ env.POSTGRES_PASSWORD }}"
run: |
echo "Re-applying migrations after rollback..."
dotnet ef database update --project "${{ steps.module.outputs.project }}"
echo "Re-apply successful!"
- name: Report rollback results
if: always()
run: |
echo "## Rollback Test: ${{ steps.module.outputs.name }}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
if [[ -n "${{ steps.migrations.outputs.rollback_to }}" ]]; then
echo "- Rollback target: ${{ steps.migrations.outputs.rollback_to }}" >> $GITHUB_STEP_SUMMARY
echo "- Status: Tested" >> $GITHUB_STEP_SUMMARY
else
echo "- Status: Skipped (insufficient migrations)" >> $GITHUB_STEP_SUMMARY
fi
# ===========================================================================
# IDEMPOTENCY TESTS
# ===========================================================================
idempotency:
name: Idempotency Test
runs-on: ubuntu-22.04
timeout-minutes: 20
needs: [discover, forward-migrations]
if: |
needs.discover.outputs.module_count != '0' &&
(github.event_name == 'schedule' || github.event.inputs.test_idempotency == 'true')
services:
postgres:
image: postgres:16
env:
POSTGRES_USER: ${{ env.POSTGRES_USER }}
POSTGRES_PASSWORD: ${{ env.POSTGRES_PASSWORD }}
POSTGRES_DB: ${{ env.POSTGRES_DB }}
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
strategy:
fail-fast: false
matrix:
module: ${{ fromJson(needs.discover.outputs.modules) }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: ${{ env.DOTNET_VERSION }}
include-prerelease: true
- name: Install EF Core tools
run: dotnet tool install -g dotnet-ef
- name: Get module info
id: module
run: |
MODULE_NAME=$(basename $(dirname "${{ matrix.module }}"))
echo "name=$MODULE_NAME" >> $GITHUB_OUTPUT
PROJECT_FILE=$(find "${{ matrix.module }}" -maxdepth 1 -name "*.csproj" | head -1)
echo "project=$PROJECT_FILE" >> $GITHUB_OUTPUT
- name: Setup database
env:
ConnectionStrings__Default: "Host=${{ env.POSTGRES_HOST }};Port=${{ env.POSTGRES_PORT }};Database=${{ env.POSTGRES_DB }}_idem_${{ steps.module.outputs.name }};Username=${{ env.POSTGRES_USER }};Password=${{ env.POSTGRES_PASSWORD }}"
PGPASSWORD: ${{ env.POSTGRES_PASSWORD }}
run: |
psql -h ${{ env.POSTGRES_HOST }} -U ${{ env.POSTGRES_USER }} -d postgres \
-c "DROP DATABASE IF EXISTS ${{ env.POSTGRES_DB }}_idem_${{ steps.module.outputs.name }};"
psql -h ${{ env.POSTGRES_HOST }} -U ${{ env.POSTGRES_USER }} -d postgres \
-c "CREATE DATABASE ${{ env.POSTGRES_DB }}_idem_${{ steps.module.outputs.name }};"
- name: First migration run
env:
ConnectionStrings__Default: "Host=${{ env.POSTGRES_HOST }};Port=${{ env.POSTGRES_PORT }};Database=${{ env.POSTGRES_DB }}_idem_${{ steps.module.outputs.name }};Username=${{ env.POSTGRES_USER }};Password=${{ env.POSTGRES_PASSWORD }}"
run: |
dotnet ef database update --project "${{ steps.module.outputs.project }}"
- name: Get initial schema hash
id: hash1
env:
PGPASSWORD: ${{ env.POSTGRES_PASSWORD }}
run: |
SCHEMA_HASH=$(psql -h ${{ env.POSTGRES_HOST }} -U ${{ env.POSTGRES_USER }} \
-d "${{ env.POSTGRES_DB }}_idem_${{ steps.module.outputs.name }}" -t -c \
"SELECT md5(string_agg(table_name || column_name || data_type, '' ORDER BY table_name, column_name))
FROM information_schema.columns WHERE table_schema = 'public';")
echo "hash=$SCHEMA_HASH" >> $GITHUB_OUTPUT
echo "Initial schema hash: $SCHEMA_HASH"
- name: Second migration run (idempotency test)
env:
ConnectionStrings__Default: "Host=${{ env.POSTGRES_HOST }};Port=${{ env.POSTGRES_PORT }};Database=${{ env.POSTGRES_DB }}_idem_${{ steps.module.outputs.name }};Username=${{ env.POSTGRES_USER }};Password=${{ env.POSTGRES_PASSWORD }}"
run: |
# Running migrations again should be a no-op
dotnet ef database update --project "${{ steps.module.outputs.project }}"
- name: Get final schema hash
id: hash2
env:
PGPASSWORD: ${{ env.POSTGRES_PASSWORD }}
run: |
SCHEMA_HASH=$(psql -h ${{ env.POSTGRES_HOST }} -U ${{ env.POSTGRES_USER }} \
-d "${{ env.POSTGRES_DB }}_idem_${{ steps.module.outputs.name }}" -t -c \
"SELECT md5(string_agg(table_name || column_name || data_type, '' ORDER BY table_name, column_name))
FROM information_schema.columns WHERE table_schema = 'public';")
echo "hash=$SCHEMA_HASH" >> $GITHUB_OUTPUT
echo "Final schema hash: $SCHEMA_HASH"
- name: Verify idempotency
run: |
HASH1="${{ steps.hash1.outputs.hash }}"
HASH2="${{ steps.hash2.outputs.hash }}"
echo "## Idempotency Test: ${{ steps.module.outputs.name }}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "- Initial schema hash: $HASH1" >> $GITHUB_STEP_SUMMARY
echo "- Final schema hash: $HASH2" >> $GITHUB_STEP_SUMMARY
if [[ "$HASH1" == "$HASH2" ]]; then
echo "- Result: PASS (schemas identical)" >> $GITHUB_STEP_SUMMARY
else
echo "- Result: FAIL (schemas differ)" >> $GITHUB_STEP_SUMMARY
echo "::error::Idempotency test failed for ${{ steps.module.outputs.name }}"
exit 1
fi
# ===========================================================================
# SUMMARY
# ===========================================================================
summary:
name: Migration Summary
runs-on: ubuntu-22.04
needs: [discover, forward-migrations, rollback-migrations, idempotency]
if: always()
steps:
- name: Generate Summary
run: |
echo "## Migration Test Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Test | Status |" >> $GITHUB_STEP_SUMMARY
echo "|------|--------|" >> $GITHUB_STEP_SUMMARY
echo "| Discovery | ${{ needs.discover.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Forward Migrations | ${{ needs.forward-migrations.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Rollback Migrations | ${{ needs.rollback-migrations.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Idempotency | ${{ needs.idempotency.result }} |" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Modules Tested: ${{ needs.discover.outputs.module_count }}" >> $GITHUB_STEP_SUMMARY
- name: Check for failures
if: contains(needs.*.result, 'failure')
run: exit 1

View File

@@ -0,0 +1,483 @@
# .gitea/workflows/nightly-regression.yml
# Nightly Full-Suite Regression Testing
# Sprint: CI/CD Enhancement - Comprehensive Testing
#
# Purpose: Run comprehensive regression tests that are too expensive for PR gating
# - Full test matrix (all categories)
# - Extended integration tests
# - Performance benchmarks with historical comparison
# - Cross-module dependency validation
# - Determinism verification
#
# Schedule: Daily at 2:00 AM UTC (off-peak hours)
#
# Notifications: Slack/Teams on failure
name: Nightly Regression
on:
schedule:
- cron: '0 2 * * *' # Daily at 2:00 AM UTC
workflow_dispatch:
inputs:
skip_performance:
description: 'Skip performance tests'
type: boolean
default: false
skip_determinism:
description: 'Skip determinism tests'
type: boolean
default: false
notify_on_success:
description: 'Send notification on success'
type: boolean
default: false
env:
DOTNET_VERSION: '10.0.100'
DOTNET_NOLOGO: 1
DOTNET_CLI_TELEMETRY_OPTOUT: 1
DOTNET_SYSTEM_GLOBALIZATION_INVARIANT: 1
TZ: UTC
jobs:
# ===========================================================================
# PREPARE NIGHTLY RUN
# ===========================================================================
prepare:
name: Prepare Nightly Run
runs-on: ubuntu-22.04
outputs:
run_id: ${{ steps.metadata.outputs.run_id }}
run_date: ${{ steps.metadata.outputs.run_date }}
commit_sha: ${{ steps.metadata.outputs.commit_sha }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Generate run metadata
id: metadata
run: |
RUN_ID="nightly-$(date -u +%Y%m%d-%H%M%S)"
RUN_DATE=$(date -u +%Y-%m-%d)
COMMIT_SHA=$(git rev-parse HEAD)
echo "run_id=$RUN_ID" >> $GITHUB_OUTPUT
echo "run_date=$RUN_DATE" >> $GITHUB_OUTPUT
echo "commit_sha=$COMMIT_SHA" >> $GITHUB_OUTPUT
echo "## Nightly Regression Run" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "- **Run ID:** $RUN_ID" >> $GITHUB_STEP_SUMMARY
echo "- **Date:** $RUN_DATE" >> $GITHUB_STEP_SUMMARY
echo "- **Commit:** $COMMIT_SHA" >> $GITHUB_STEP_SUMMARY
- name: Check recent commits
run: |
echo "### Recent Commits" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
git log --oneline -10 >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
# ===========================================================================
# FULL BUILD VERIFICATION
# ===========================================================================
build:
name: Full Build
runs-on: ubuntu-22.04
timeout-minutes: 30
needs: prepare
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: ${{ env.DOTNET_VERSION }}
include-prerelease: true
- name: Restore dependencies
run: dotnet restore src/StellaOps.sln
- name: Build solution (Release)
run: |
START_TIME=$(date +%s)
dotnet build src/StellaOps.sln --configuration Release --no-restore
END_TIME=$(date +%s)
DURATION=$((END_TIME - START_TIME))
echo "build_time=$DURATION" >> $GITHUB_ENV
echo "Build completed in ${DURATION}s"
- name: Report build metrics
run: |
echo "### Build Metrics" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "- **Build Time:** ${{ env.build_time }}s" >> $GITHUB_STEP_SUMMARY
echo "- **Configuration:** Release" >> $GITHUB_STEP_SUMMARY
# ===========================================================================
# COMPREHENSIVE TEST SUITE
# ===========================================================================
test-pr-gating:
name: PR-Gating Tests
runs-on: ubuntu-22.04
timeout-minutes: 45
needs: build
services:
postgres:
image: postgres:16
env:
POSTGRES_USER: stellaops
POSTGRES_PASSWORD: stellaops
POSTGRES_DB: stellaops_test
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
strategy:
fail-fast: false
matrix:
category:
- Unit
- Architecture
- Contract
- Integration
- Security
- Golden
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: ${{ env.DOTNET_VERSION }}
include-prerelease: true
- name: Run ${{ matrix.category }} Tests
env:
STELLAOPS_TEST_POSTGRES_CONNECTION: "Host=localhost;Port=5432;Database=stellaops_test;Username=stellaops;Password=stellaops"
run: |
chmod +x .gitea/scripts/test/run-test-category.sh
.gitea/scripts/test/run-test-category.sh "${{ matrix.category }}"
- name: Upload Test Results
uses: actions/upload-artifact@v4
if: always()
with:
name: nightly-test-${{ matrix.category }}
path: ./TestResults/${{ matrix.category }}
retention-days: 30
test-extended:
name: Extended Tests
runs-on: ubuntu-22.04
timeout-minutes: 60
needs: build
if: github.event.inputs.skip_performance != 'true'
strategy:
fail-fast: false
matrix:
category:
- Performance
- Benchmark
- Resilience
- Observability
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: ${{ env.DOTNET_VERSION }}
include-prerelease: true
- name: Run ${{ matrix.category }} Tests
run: |
chmod +x .gitea/scripts/test/run-test-category.sh
.gitea/scripts/test/run-test-category.sh "${{ matrix.category }}"
- name: Upload Test Results
uses: actions/upload-artifact@v4
if: always()
with:
name: nightly-extended-${{ matrix.category }}
path: ./TestResults/${{ matrix.category }}
retention-days: 30
# ===========================================================================
# DETERMINISM VERIFICATION
# ===========================================================================
determinism:
name: Determinism Verification
runs-on: ubuntu-22.04
timeout-minutes: 45
needs: build
if: github.event.inputs.skip_determinism != 'true'
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: ${{ env.DOTNET_VERSION }}
include-prerelease: true
- name: First build
run: |
dotnet build src/StellaOps.sln --configuration Release -o ./build-1
find ./build-1 -name "*.dll" -exec sha256sum {} \; | sort > checksums-1.txt
- name: Clean and rebuild
run: |
rm -rf ./build-1
dotnet clean src/StellaOps.sln
dotnet build src/StellaOps.sln --configuration Release -o ./build-2
find ./build-2 -name "*.dll" -exec sha256sum {} \; | sort > checksums-2.txt
- name: Compare builds
id: compare
run: |
echo "### Determinism Check" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
if diff checksums-1.txt checksums-2.txt > /dev/null; then
echo "PASS: Builds are deterministic" >> $GITHUB_STEP_SUMMARY
echo "deterministic=true" >> $GITHUB_OUTPUT
else
echo "FAIL: Builds differ" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "<details><summary>Differences</summary>" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo '```diff' >> $GITHUB_STEP_SUMMARY
diff checksums-1.txt checksums-2.txt >> $GITHUB_STEP_SUMMARY || true
echo '```' >> $GITHUB_STEP_SUMMARY
echo "</details>" >> $GITHUB_STEP_SUMMARY
echo "deterministic=false" >> $GITHUB_OUTPUT
exit 1
fi
- name: Upload checksums
uses: actions/upload-artifact@v4
if: always()
with:
name: nightly-determinism-checksums
path: checksums-*.txt
retention-days: 30
# ===========================================================================
# CROSS-MODULE VALIDATION
# ===========================================================================
cross-module:
name: Cross-Module Validation
runs-on: ubuntu-22.04
timeout-minutes: 30
needs: build
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: ${{ env.DOTNET_VERSION }}
include-prerelease: true
- name: Check for circular dependencies
run: |
echo "### Dependency Analysis" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
# Build dependency graph
echo "Analyzing project dependencies..."
for proj in $(find src -name "*.csproj" ! -path "*/bin/*" ! -path "*/obj/*" | head -50); do
# Extract ProjectReference entries
refs=$(grep -oP 'ProjectReference Include="\K[^"]+' "$proj" 2>/dev/null || true)
if [[ -n "$refs" ]]; then
basename "$proj" >> deps.txt
echo "$refs" | while read ref; do
echo " -> $(basename "$ref")" >> deps.txt
done
fi
done
if [[ -f deps.txt ]]; then
echo "<details><summary>Project Dependencies (first 50)</summary>" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
head -100 deps.txt >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
echo "</details>" >> $GITHUB_STEP_SUMMARY
fi
- name: Validate no deprecated APIs
run: |
# Check for use of deprecated patterns
DEPRECATED_COUNT=$(grep -r "Obsolete" src --include="*.cs" | wc -l || echo "0")
echo "- Obsolete attribute usages: $DEPRECATED_COUNT" >> $GITHUB_STEP_SUMMARY
# ===========================================================================
# CODE COVERAGE REPORT
# ===========================================================================
coverage:
name: Code Coverage
runs-on: ubuntu-22.04
timeout-minutes: 45
needs: build
services:
postgres:
image: postgres:16
env:
POSTGRES_USER: stellaops
POSTGRES_PASSWORD: stellaops
POSTGRES_DB: stellaops_test
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: ${{ env.DOTNET_VERSION }}
include-prerelease: true
- name: Run tests with coverage
env:
STELLAOPS_TEST_POSTGRES_CONNECTION: "Host=localhost;Port=5432;Database=stellaops_test;Username=stellaops;Password=stellaops"
run: |
dotnet test src/StellaOps.sln \
--configuration Release \
--collect:"XPlat Code Coverage" \
--results-directory ./TestResults/Coverage \
--filter "Category=Unit|Category=Integration" \
--verbosity minimal \
-- DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Format=cobertura
- name: Install ReportGenerator
run: dotnet tool install -g dotnet-reportgenerator-globaltool
- name: Generate coverage report
run: |
reportgenerator \
-reports:"./TestResults/Coverage/**/coverage.cobertura.xml" \
-targetdir:"./TestResults/CoverageReport" \
-reporttypes:"Html;MarkdownSummary;Cobertura" \
|| true
- name: Add coverage to summary
run: |
echo "### Code Coverage Report" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
if [[ -f "./TestResults/CoverageReport/Summary.md" ]]; then
cat "./TestResults/CoverageReport/Summary.md" >> $GITHUB_STEP_SUMMARY
else
echo "Coverage report generation failed or no coverage data collected." >> $GITHUB_STEP_SUMMARY
fi
- name: Upload coverage report
uses: actions/upload-artifact@v4
if: always()
with:
name: nightly-coverage-report
path: ./TestResults/CoverageReport
retention-days: 30
# ===========================================================================
# SUMMARY AND NOTIFICATION
# ===========================================================================
summary:
name: Nightly Summary
runs-on: ubuntu-22.04
needs:
- prepare
- build
- test-pr-gating
- test-extended
- determinism
- cross-module
- coverage
if: always()
steps:
- name: Generate final summary
run: |
echo "## Nightly Regression Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Run ID:** ${{ needs.prepare.outputs.run_id }}" >> $GITHUB_STEP_SUMMARY
echo "**Date:** ${{ needs.prepare.outputs.run_date }}" >> $GITHUB_STEP_SUMMARY
echo "**Commit:** ${{ needs.prepare.outputs.commit_sha }}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Job Results" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Job | Status |" >> $GITHUB_STEP_SUMMARY
echo "|-----|--------|" >> $GITHUB_STEP_SUMMARY
echo "| Build | ${{ needs.build.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| PR-Gating Tests | ${{ needs.test-pr-gating.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Extended Tests | ${{ needs.test-extended.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Determinism | ${{ needs.determinism.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Cross-Module | ${{ needs.cross-module.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Coverage | ${{ needs.coverage.result }} |" >> $GITHUB_STEP_SUMMARY
- name: Determine overall status
id: status
run: |
if [[ "${{ needs.build.result }}" == "failure" ]] || \
[[ "${{ needs.test-pr-gating.result }}" == "failure" ]] || \
[[ "${{ needs.determinism.result }}" == "failure" ]]; then
echo "status=failure" >> $GITHUB_OUTPUT
else
echo "status=success" >> $GITHUB_OUTPUT
fi
# Placeholder for notifications - configure webhook URL in secrets
- name: Send failure notification
if: steps.status.outputs.status == 'failure'
run: |
echo "::warning::Nightly regression failed - notification would be sent here"
# Uncomment and configure when webhook is available:
# curl -X POST "${{ secrets.SLACK_WEBHOOK_URL }}" \
# -H "Content-Type: application/json" \
# -d '{
# "text": "Nightly Regression Failed",
# "attachments": [{
# "color": "danger",
# "fields": [
# {"title": "Run ID", "value": "${{ needs.prepare.outputs.run_id }}", "short": true},
# {"title": "Commit", "value": "${{ needs.prepare.outputs.commit_sha }}", "short": true}
# ]
# }]
# }'
- name: Send success notification
if: steps.status.outputs.status == 'success' && github.event.inputs.notify_on_success == 'true'
run: |
echo "::notice::Nightly regression passed"
- name: Exit with appropriate code
if: steps.status.outputs.status == 'failure'
run: exit 1

View File

@@ -532,6 +532,233 @@ jobs:
path: out/release
retention-days: 90
# ===========================================================================
# GENERATE CHANGELOG (AI-assisted)
# ===========================================================================
generate-changelog:
name: Generate Changelog
runs-on: ubuntu-22.04
needs: [validate, build-modules]
if: always() && needs.validate.result == 'success'
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Find previous release tag
id: prev-tag
run: |
PREV_TAG=$(git tag -l "suite-*" --sort=-creatordate | head -1)
echo "Previous tag: ${PREV_TAG:-none}"
echo "prev_tag=${PREV_TAG}" >> $GITHUB_OUTPUT
- name: Generate changelog
env:
AI_API_KEY: ${{ secrets.AI_API_KEY }}
run: |
VERSION="${{ needs.validate.outputs.version }}"
CODENAME="${{ needs.validate.outputs.codename }}"
PREV_TAG="${{ steps.prev-tag.outputs.prev_tag }}"
mkdir -p out/docs
ARGS="$VERSION --codename $CODENAME --output out/docs/CHANGELOG.md"
if [[ -n "$PREV_TAG" ]]; then
ARGS="$ARGS --from-tag $PREV_TAG"
fi
if [[ -n "$AI_API_KEY" ]]; then
ARGS="$ARGS --ai"
fi
python3 .gitea/scripts/release/generate_changelog.py $ARGS
echo "=== Generated Changelog ==="
head -50 out/docs/CHANGELOG.md
- name: Upload changelog
uses: actions/upload-artifact@v4
with:
name: changelog-${{ needs.validate.outputs.version }}
path: out/docs/CHANGELOG.md
retention-days: 90
# ===========================================================================
# GENERATE SUITE DOCUMENTATION
# ===========================================================================
generate-suite-docs:
name: Generate Suite Docs
runs-on: ubuntu-22.04
needs: [validate, generate-changelog, release-manifest]
if: always() && needs.validate.result == 'success'
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install dependencies
run: pip install python-dateutil
- name: Download changelog
uses: actions/download-artifact@v4
with:
name: changelog-${{ needs.validate.outputs.version }}
path: changelog
- name: Find previous version
id: prev-version
run: |
PREV_TAG=$(git tag -l "suite-*" --sort=-creatordate | head -1)
if [[ -n "$PREV_TAG" ]]; then
PREV_VERSION=$(echo "$PREV_TAG" | sed 's/suite-//')
echo "prev_version=$PREV_VERSION" >> $GITHUB_OUTPUT
fi
- name: Generate suite documentation
run: |
VERSION="${{ needs.validate.outputs.version }}"
CODENAME="${{ needs.validate.outputs.codename }}"
CHANNEL="${{ needs.validate.outputs.channel }}"
PREV="${{ steps.prev-version.outputs.prev_version }}"
ARGS="$VERSION $CODENAME --channel $CHANNEL"
if [[ -f "changelog/CHANGELOG.md" ]]; then
ARGS="$ARGS --changelog changelog/CHANGELOG.md"
fi
if [[ -n "$PREV" ]]; then
ARGS="$ARGS --previous $PREV"
fi
python3 .gitea/scripts/release/generate_suite_docs.py $ARGS
echo "=== Generated Documentation ==="
ls -la docs/releases/$VERSION/
- name: Upload suite docs
uses: actions/upload-artifact@v4
with:
name: suite-docs-${{ needs.validate.outputs.version }}
path: docs/releases/${{ needs.validate.outputs.version }}
retention-days: 90
# ===========================================================================
# GENERATE DOCKER COMPOSE FILES
# ===========================================================================
generate-compose:
name: Generate Docker Compose
runs-on: ubuntu-22.04
needs: [validate, release-manifest]
if: always() && needs.validate.result == 'success'
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Generate Docker Compose files
run: |
VERSION="${{ needs.validate.outputs.version }}"
CODENAME="${{ needs.validate.outputs.codename }}"
mkdir -p out/compose
# Standard compose
python3 .gitea/scripts/release/generate_compose.py \
"$VERSION" "$CODENAME" \
--output out/compose/docker-compose.yml
# Air-gap variant
python3 .gitea/scripts/release/generate_compose.py \
"$VERSION" "$CODENAME" \
--airgap \
--output out/compose/docker-compose.airgap.yml
echo "=== Generated Compose Files ==="
ls -la out/compose/
- name: Upload compose files
uses: actions/upload-artifact@v4
with:
name: compose-${{ needs.validate.outputs.version }}
path: out/compose
retention-days: 90
# ===========================================================================
# COMMIT DOCS TO REPOSITORY
# ===========================================================================
commit-docs:
name: Commit Documentation
runs-on: ubuntu-22.04
needs: [validate, generate-suite-docs, generate-compose, create-release]
if: needs.validate.outputs.dry_run != 'true' && needs.create-release.result == 'success'
steps:
- name: Checkout
uses: actions/checkout@v4
with:
token: ${{ secrets.GITEA_TOKEN }}
fetch-depth: 0
- name: Download suite docs
uses: actions/download-artifact@v4
with:
name: suite-docs-${{ needs.validate.outputs.version }}
path: docs/releases/${{ needs.validate.outputs.version }}
- name: Download compose files
uses: actions/download-artifact@v4
with:
name: compose-${{ needs.validate.outputs.version }}
path: docs/releases/${{ needs.validate.outputs.version }}
- name: Commit documentation
run: |
VERSION="${{ needs.validate.outputs.version }}"
CODENAME="${{ needs.validate.outputs.codename }}"
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add "docs/releases/${VERSION}"
if git diff --cached --quiet; then
echo "No documentation changes to commit"
else
git commit -m "docs: add release documentation for ${VERSION} ${CODENAME}
Generated documentation for StellaOps ${VERSION} \"${CODENAME}\"
- README.md
- CHANGELOG.md
- services.md
- upgrade-guide.md
- docker-compose.yml
- docker-compose.airgap.yml
- manifest.yaml
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: github-actions[bot] <github-actions[bot]@users.noreply.github.com>"
git push
echo "Documentation committed and pushed"
fi
# ===========================================================================
# CREATE GITEA RELEASE
# ===========================================================================
@@ -651,7 +878,7 @@ jobs:
summary:
name: Release Summary
runs-on: ubuntu-22.04
needs: [validate, build-modules, build-containers, build-cli, build-helm, release-manifest, create-release]
needs: [validate, build-modules, build-containers, build-cli, build-helm, release-manifest, generate-changelog, generate-suite-docs, generate-compose, create-release, commit-docs]
if: always()
steps:
- name: Generate Summary
@@ -674,7 +901,11 @@ jobs:
echo "| Build CLI | ${{ needs.build-cli.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Build Helm | ${{ needs.build-helm.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Release Manifest | ${{ needs.release-manifest.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Generate Changelog | ${{ needs.generate-changelog.result || 'skipped' }} |" >> $GITHUB_STEP_SUMMARY
echo "| Generate Suite Docs | ${{ needs.generate-suite-docs.result || 'skipped' }} |" >> $GITHUB_STEP_SUMMARY
echo "| Generate Compose | ${{ needs.generate-compose.result || 'skipped' }} |" >> $GITHUB_STEP_SUMMARY
echo "| Create Release | ${{ needs.create-release.result || 'skipped' }} |" >> $GITHUB_STEP_SUMMARY
echo "| Commit Documentation | ${{ needs.commit-docs.result || 'skipped' }} |" >> $GITHUB_STEP_SUMMARY
- name: Check for failures
if: contains(needs.*.result, 'failure')

View File

@@ -0,0 +1,114 @@
# Renovate Bot Workflow for Gitea
# Sprint: CI/CD Enhancement - Dependency Management Automation
#
# Purpose: Run Renovate Bot to automatically update dependencies
# Schedule: Twice daily (03:00 and 15:00 UTC)
#
# Requirements:
# - RENOVATE_TOKEN secret with repo write access
# - renovate.json configuration in repo root
name: Renovate
on:
schedule:
# Run at 03:00 and 15:00 UTC
- cron: '0 3,15 * * *'
workflow_dispatch:
inputs:
dry_run:
description: 'Dry run (no PRs created)'
required: false
type: boolean
default: false
log_level:
description: 'Log level'
required: false
type: choice
options:
- debug
- info
- warn
default: 'info'
env:
RENOVATE_VERSION: '37.100.0'
LOG_LEVEL: ${{ github.event.inputs.log_level || 'info' }}
jobs:
renovate:
name: Run Renovate
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Validate configuration
run: |
if [[ ! -f "renovate.json" ]]; then
echo "::error::renovate.json not found in repository root"
exit 1
fi
echo "Renovate configuration found"
cat renovate.json | head -20
- name: Run Renovate
env:
RENOVATE_TOKEN: ${{ secrets.RENOVATE_TOKEN }}
RENOVATE_PLATFORM: gitea
RENOVATE_ENDPOINT: ${{ github.server_url }}/api/v1
RENOVATE_REPOSITORIES: ${{ github.repository }}
RENOVATE_DRY_RUN: ${{ github.event.inputs.dry_run == 'true' && 'full' || 'null' }}
LOG_LEVEL: ${{ env.LOG_LEVEL }}
run: |
# Install Renovate
npm install -g renovate@${{ env.RENOVATE_VERSION }}
# Configure Renovate
export RENOVATE_CONFIG_FILE="${GITHUB_WORKSPACE}/renovate.json"
# Set dry run mode
if [[ "$RENOVATE_DRY_RUN" == "full" ]]; then
echo "Running in DRY RUN mode - no PRs will be created"
export RENOVATE_DRY_RUN="full"
fi
# Run Renovate
renovate \
--platform="$RENOVATE_PLATFORM" \
--endpoint="$RENOVATE_ENDPOINT" \
--token="$RENOVATE_TOKEN" \
"$RENOVATE_REPOSITORIES" \
2>&1 | tee renovate.log
- name: Upload Renovate log
uses: actions/upload-artifact@v4
if: always()
with:
name: renovate-log-${{ github.run_id }}
path: renovate.log
retention-days: 7
- name: Summary
if: always()
run: |
echo "## Renovate Run Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Property | Value |" >> $GITHUB_STEP_SUMMARY
echo "|----------|-------|" >> $GITHUB_STEP_SUMMARY
echo "| Version | ${{ env.RENOVATE_VERSION }} |" >> $GITHUB_STEP_SUMMARY
echo "| Log Level | ${{ env.LOG_LEVEL }} |" >> $GITHUB_STEP_SUMMARY
echo "| Dry Run | ${{ github.event.inputs.dry_run || 'false' }} |" >> $GITHUB_STEP_SUMMARY
echo "| Trigger | ${{ github.event_name }} |" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
if [[ -f renovate.log ]]; then
# Count PRs created/updated
CREATED=$(grep -c "PR created" renovate.log 2>/dev/null || echo "0")
UPDATED=$(grep -c "PR updated" renovate.log 2>/dev/null || echo "0")
echo "### Results" >> $GITHUB_STEP_SUMMARY
echo "- PRs Created: $CREATED" >> $GITHUB_STEP_SUMMARY
echo "- PRs Updated: $UPDATED" >> $GITHUB_STEP_SUMMARY
fi

View File

@@ -0,0 +1,277 @@
# Emergency Rollback Workflow
# Sprint: CI/CD Enhancement - Deployment Safety
#
# Purpose: Automated rollback to previous known-good version
# Triggers: Manual dispatch only (emergency procedure)
#
# SLA Target: < 5 minutes from trigger to rollback complete
name: Emergency Rollback
on:
workflow_dispatch:
inputs:
environment:
description: 'Target environment'
required: true
type: choice
options:
- staging
- production
service:
description: 'Service to rollback (or "all" for full rollback)'
required: true
type: choice
options:
- all
- authority
- attestor
- concelier
- scanner
- policy
- excititor
- gateway
- scheduler
- cli
target_version:
description: 'Version to rollback to (leave empty for previous version)'
required: false
type: string
reason:
description: 'Reason for rollback'
required: true
type: string
skip_health_check:
description: 'Skip health check (use only in emergencies)'
required: false
type: boolean
default: false
env:
ROLLBACK_TIMEOUT: 300 # 5 minutes
jobs:
validate:
name: Validate Rollback Request
runs-on: ubuntu-latest
outputs:
target_version: ${{ steps.resolve.outputs.version }}
services: ${{ steps.resolve.outputs.services }}
approved: ${{ steps.validate.outputs.approved }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Validate inputs
id: validate
run: |
echo "## Rollback Request Validation" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Parameter | Value |" >> $GITHUB_STEP_SUMMARY
echo "|-----------|-------|" >> $GITHUB_STEP_SUMMARY
echo "| Environment | ${{ inputs.environment }} |" >> $GITHUB_STEP_SUMMARY
echo "| Service | ${{ inputs.service }} |" >> $GITHUB_STEP_SUMMARY
echo "| Target Version | ${{ inputs.target_version || 'previous' }} |" >> $GITHUB_STEP_SUMMARY
echo "| Reason | ${{ inputs.reason }} |" >> $GITHUB_STEP_SUMMARY
echo "| Triggered By | ${{ github.actor }} |" >> $GITHUB_STEP_SUMMARY
echo "| Timestamp | $(date -u +"%Y-%m-%dT%H:%M:%SZ") |" >> $GITHUB_STEP_SUMMARY
# Production requires additional validation
if [[ "${{ inputs.environment }}" == "production" ]]; then
echo ""
echo "### Production Rollback Warning" >> $GITHUB_STEP_SUMMARY
echo "This will affect production users immediately." >> $GITHUB_STEP_SUMMARY
fi
echo "approved=true" >> $GITHUB_OUTPUT
- name: Resolve target version
id: resolve
run: |
VERSION="${{ inputs.target_version }}"
SERVICE="${{ inputs.service }}"
# If no version specified, get previous from manifest
if [[ -z "$VERSION" ]]; then
MANIFEST="devops/releases/service-versions.json"
if [[ -f "$MANIFEST" ]]; then
if [[ "$SERVICE" == "all" ]]; then
# Get oldest version across all services
VERSION=$(jq -r '.services | to_entries | map(.value.version) | sort | first // "unknown"' "$MANIFEST")
else
VERSION=$(jq -r --arg svc "$SERVICE" '.services[$svc].previousVersion // .services[$svc].version // "unknown"' "$MANIFEST")
fi
fi
fi
# Determine services to rollback
if [[ "$SERVICE" == "all" ]]; then
SERVICES='["authority","attestor","concelier","scanner","policy","excititor","gateway","scheduler"]'
else
SERVICES="[\"$SERVICE\"]"
fi
echo "Resolved version: $VERSION"
echo "Services: $SERVICES"
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "services=$SERVICES" >> $GITHUB_OUTPUT
rollback:
name: Execute Rollback
runs-on: ubuntu-latest
needs: [validate]
if: needs.validate.outputs.approved == 'true'
environment: ${{ inputs.environment }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup kubectl
uses: azure/setup-kubectl@v3
with:
version: 'latest'
- name: Setup Helm
uses: azure/setup-helm@v3
with:
version: 'latest'
- name: Configure deployment access
run: |
echo "::notice::Configure deployment access for ${{ inputs.environment }}"
# TODO: Configure kubectl context / kubeconfig
# kubectl config use-context ${{ inputs.environment }}
- name: Execute rollback
id: rollback
run: |
echo "Starting rollback..."
START_TIME=$(date +%s)
TARGET_VERSION="${{ needs.validate.outputs.target_version }}"
SERVICES='${{ needs.validate.outputs.services }}'
ENVIRONMENT="${{ inputs.environment }}"
# Execute rollback script
if [[ -f ".gitea/scripts/release/rollback.sh" ]]; then
.gitea/scripts/release/rollback.sh \
--environment "$ENVIRONMENT" \
--version "$TARGET_VERSION" \
--services "$SERVICES" \
--reason "${{ inputs.reason }}"
else
echo "::warning::Rollback script not found - using placeholder"
echo ""
echo "Rollback would execute:"
echo " Environment: $ENVIRONMENT"
echo " Version: $TARGET_VERSION"
echo " Services: $SERVICES"
echo ""
echo "TODO: Implement rollback.sh script"
fi
END_TIME=$(date +%s)
DURATION=$((END_TIME - START_TIME))
echo "duration=$DURATION" >> $GITHUB_OUTPUT
echo "Rollback completed in ${DURATION}s"
- name: Health check
if: inputs.skip_health_check != true
run: |
echo "Running health checks..."
SERVICES='${{ needs.validate.outputs.services }}'
echo "$SERVICES" | jq -r '.[]' | while read -r service; do
echo "Checking $service..."
# TODO: Implement service-specific health checks
# curl -sf "https://${service}.${{ inputs.environment }}.stella-ops.org/health" || exit 1
echo " Status: OK (placeholder)"
done
echo "All health checks passed"
- name: Rollback summary
if: always()
run: |
echo "" >> $GITHUB_STEP_SUMMARY
echo "## Rollback Execution" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
if [[ "${{ steps.rollback.outcome }}" == "success" ]]; then
echo "### Rollback Successful" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "- Duration: ${{ steps.rollback.outputs.duration }}s" >> $GITHUB_STEP_SUMMARY
echo "- Target Version: ${{ needs.validate.outputs.target_version }}" >> $GITHUB_STEP_SUMMARY
else
echo "### Rollback Failed" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "Please investigate immediately and consider manual intervention." >> $GITHUB_STEP_SUMMARY
fi
notify:
name: Send Notifications
runs-on: ubuntu-latest
needs: [validate, rollback]
if: always()
steps:
- name: Notify team
run: |
STATUS="${{ needs.rollback.result }}"
ENVIRONMENT="${{ inputs.environment }}"
SERVICE="${{ inputs.service }}"
ACTOR="${{ github.actor }}"
REASON="${{ inputs.reason }}"
VERSION="${{ needs.validate.outputs.target_version }}"
# Build notification message
if [[ "$STATUS" == "success" ]]; then
EMOJI="white_check_mark"
TITLE="Rollback Completed Successfully"
else
EMOJI="x"
TITLE="Rollback Failed - Immediate Attention Required"
fi
echo "Notification:"
echo " Title: $TITLE"
echo " Environment: $ENVIRONMENT"
echo " Service: $SERVICE"
echo " Version: $VERSION"
echo " Actor: $ACTOR"
echo " Reason: $REASON"
# TODO: Send to Slack/Teams/PagerDuty
# - name: Slack notification
# uses: slackapi/slack-github-action@v1
# with:
# payload: |
# {
# "text": "${{ env.TITLE }}",
# "blocks": [...]
# }
- name: Create incident record
run: |
echo "Creating incident record..."
# Log to incident tracking
INCIDENT_LOG="devops/incidents/$(date +%Y-%m-%d)-rollback.json"
echo "{
\"timestamp\": \"$(date -u +"%Y-%m-%dT%H:%M:%SZ")\",
\"type\": \"rollback\",
\"environment\": \"${{ inputs.environment }}\",
\"service\": \"${{ inputs.service }}\",
\"target_version\": \"${{ needs.validate.outputs.target_version }}\",
\"reason\": \"${{ inputs.reason }}\",
\"actor\": \"${{ github.actor }}\",
\"status\": \"${{ needs.rollback.result }}\",
\"run_id\": \"${{ github.run_id }}\"
}"
echo "::notice::Incident record would be created at $INCIDENT_LOG"

View File

@@ -0,0 +1,386 @@
# .gitea/workflows/sast-scan.yml
# Static Application Security Testing (SAST) Workflow
# Sprint: CI/CD Enhancement - Security Scanning (Tier 2)
#
# Purpose: Detect security vulnerabilities in source code through static analysis
# - Code injection vulnerabilities
# - Authentication/authorization issues
# - Cryptographic weaknesses
# - Data exposure risks
# - OWASP Top 10 detection
#
# Supported Languages: C#/.NET, JavaScript/TypeScript, Python, YAML, Dockerfile
#
# PLACEHOLDER: Choose your SAST scanner implementation below
# Options:
# 1. Semgrep - Fast, open-source, good .NET support
# 2. CodeQL - GitHub's analysis engine
# 3. SonarQube - Enterprise-grade with dashboards
# 4. Snyk Code - Commercial with good accuracy
name: SAST Scanning
on:
push:
branches: [main, develop]
paths:
- 'src/**'
- '*.csproj'
- '*.cs'
- '*.ts'
- '*.js'
- '*.py'
- 'Dockerfile*'
pull_request:
paths:
- 'src/**'
- '*.csproj'
- '*.cs'
- '*.ts'
- '*.js'
- '*.py'
- 'Dockerfile*'
schedule:
- cron: '30 3 * * 1' # Weekly on Monday at 3:30 AM UTC
workflow_dispatch:
inputs:
scan_level:
description: 'Scan thoroughness level'
type: choice
options:
- quick
- standard
- comprehensive
default: standard
fail_on_findings:
description: 'Fail workflow on findings'
type: boolean
default: true
env:
DOTNET_VERSION: '10.0.100'
TZ: UTC
jobs:
# ===========================================================================
# PLACEHOLDER SAST IMPLEMENTATION
# ===========================================================================
#
# IMPORTANT: Configure your preferred SAST tool by uncommenting ONE of the
# implementation options below. Each option includes the necessary steps
# and configuration for that specific tool.
#
# ===========================================================================
sast-scan:
name: SAST Analysis
runs-on: ubuntu-22.04
timeout-minutes: 30
permissions:
security-events: write
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
# =========================================================================
# PLACEHOLDER: Uncomment your preferred SAST tool configuration
# =========================================================================
- name: SAST Scan Placeholder
run: |
echo "::notice::SAST scanning placeholder - configure your scanner below"
echo ""
echo "Available SAST options:"
echo ""
echo "1. SEMGREP (Recommended for open-source)"
echo " Uncomment the Semgrep section below"
echo " - Fast, accurate, good .NET support"
echo " - Free for open-source projects"
echo ""
echo "2. CODEQL (GitHub native)"
echo " Uncomment the CodeQL section below"
echo " - Deep analysis capabilities"
echo " - Native GitHub integration"
echo ""
echo "3. SONARQUBE (Enterprise)"
echo " Uncomment the SonarQube section below"
echo " - Comprehensive dashboards"
echo " - Technical debt tracking"
echo ""
echo "4. SNYK CODE (Commercial)"
echo " Uncomment the Snyk section below"
echo " - High accuracy"
echo " - Good IDE integration"
# =========================================================================
# OPTION 1: SEMGREP
# =========================================================================
# Uncomment the following section to use Semgrep:
#
# - name: Run Semgrep
# uses: returntocorp/semgrep-action@v1
# with:
# config: >-
# p/default
# p/security-audit
# p/owasp-top-ten
# p/csharp
# p/javascript
# p/typescript
# p/python
# p/docker
# env:
# SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }}
# =========================================================================
# OPTION 2: CODEQL
# =========================================================================
# Uncomment the following section to use CodeQL:
#
# - name: Initialize CodeQL
# uses: github/codeql-action/init@v3
# with:
# languages: csharp, javascript
# queries: security-and-quality
#
# - name: Build for CodeQL
# run: |
# dotnet build src/StellaOps.sln --configuration Release
#
# - name: Perform CodeQL Analysis
# uses: github/codeql-action/analyze@v3
# with:
# category: "/language:csharp"
# =========================================================================
# OPTION 3: SONARQUBE
# =========================================================================
# Uncomment the following section to use SonarQube:
#
# - name: SonarQube Scan
# uses: SonarSource/sonarqube-scan-action@master
# env:
# SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
# SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
# with:
# args: >
# -Dsonar.projectKey=stellaops
# -Dsonar.sources=src/
# -Dsonar.exclusions=**/bin/**,**/obj/**,**/node_modules/**
# =========================================================================
# OPTION 4: SNYK CODE
# =========================================================================
# Uncomment the following section to use Snyk Code:
#
# - name: Setup Snyk
# uses: snyk/actions/setup@master
#
# - name: Snyk Code Test
# run: snyk code test --sarif-file-output=snyk-code.sarif
# env:
# SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
# continue-on-error: true
#
# - name: Upload Snyk results
# uses: github/codeql-action/upload-sarif@v3
# with:
# sarif_file: snyk-code.sarif
# ===========================================================================
# .NET SECURITY ANALYSIS (built-in)
# ===========================================================================
dotnet-security:
name: .NET Security Analysis
runs-on: ubuntu-22.04
timeout-minutes: 20
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: ${{ env.DOTNET_VERSION }}
include-prerelease: true
- name: Restore packages
run: dotnet restore src/StellaOps.sln
- name: Run Security Code Analysis
run: |
# Enable nullable reference types warnings as errors for security
dotnet build src/StellaOps.sln \
--configuration Release \
--no-restore \
/p:TreatWarningsAsErrors=false \
/p:EnableNETAnalyzers=true \
/p:AnalysisLevel=latest \
/warnaserror:CA2100,CA2109,CA2119,CA2153,CA2300,CA2301,CA2302,CA2305,CA2310,CA2311,CA2312,CA2315,CA2321,CA2322,CA2326,CA2327,CA2328,CA2329,CA2330,CA2350,CA2351,CA2352,CA2353,CA2354,CA2355,CA2356,CA2361,CA2362,CA3001,CA3002,CA3003,CA3004,CA3005,CA3006,CA3007,CA3008,CA3009,CA3010,CA3011,CA3012,CA3061,CA3075,CA3076,CA3077,CA3147,CA5350,CA5351,CA5358,CA5359,CA5360,CA5361,CA5362,CA5363,CA5364,CA5365,CA5366,CA5367,CA5368,CA5369,CA5370,CA5371,CA5372,CA5373,CA5374,CA5375,CA5376,CA5377,CA5378,CA5379,CA5380,CA5381,CA5382,CA5383,CA5384,CA5385,CA5386,CA5387,CA5388,CA5389,CA5390,CA5391,CA5392,CA5393,CA5394,CA5395,CA5396,CA5397,CA5398,CA5399,CA5400,CA5401,CA5402,CA5403 \
2>&1 | tee build-security.log || true
- name: Parse security warnings
run: |
echo "### .NET Security Analysis" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
# Count security warnings
SECURITY_WARNINGS=$(grep -E "warning CA[235][0-9]{3}" build-security.log | wc -l || echo "0")
echo "- Security warnings found: $SECURITY_WARNINGS" >> $GITHUB_STEP_SUMMARY
if [[ $SECURITY_WARNINGS -gt 0 ]]; then
echo "" >> $GITHUB_STEP_SUMMARY
echo "<details><summary>Security Warnings</summary>" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
grep -E "warning CA[235][0-9]{3}" build-security.log | head -50 >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
echo "</details>" >> $GITHUB_STEP_SUMMARY
fi
- name: Upload security log
uses: actions/upload-artifact@v4
if: always()
with:
name: sast-dotnet-security-log
path: build-security.log
retention-days: 14
# ===========================================================================
# DEPENDENCY VULNERABILITY CHECK
# ===========================================================================
dependency-check:
name: Dependency Vulnerabilities
runs-on: ubuntu-22.04
timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: ${{ env.DOTNET_VERSION }}
include-prerelease: true
- name: Run vulnerability audit
run: |
echo "### Dependency Vulnerability Audit" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
# Check for known vulnerabilities in NuGet packages
dotnet list src/StellaOps.sln package --vulnerable --include-transitive 2>&1 | tee vuln-report.txt || true
# Parse results
VULN_COUNT=$(grep -c "has the following vulnerable packages" vuln-report.txt || echo "0")
if [[ $VULN_COUNT -gt 0 ]]; then
echo "::warning::Found $VULN_COUNT projects with vulnerable dependencies"
echo "- Projects with vulnerabilities: $VULN_COUNT" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "<details><summary>Vulnerability Report</summary>" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
cat vuln-report.txt >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
echo "</details>" >> $GITHUB_STEP_SUMMARY
else
echo "No known vulnerabilities found in dependencies." >> $GITHUB_STEP_SUMMARY
fi
- name: Upload vulnerability report
uses: actions/upload-artifact@v4
if: always()
with:
name: sast-vulnerability-report
path: vuln-report.txt
retention-days: 14
# ===========================================================================
# DOCKERFILE SECURITY LINTING
# ===========================================================================
dockerfile-lint:
name: Dockerfile Security
runs-on: ubuntu-22.04
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Find Dockerfiles
id: find
run: |
DOCKERFILES=$(find . -name "Dockerfile*" -type f ! -path "./node_modules/*" | jq -R -s -c 'split("\n") | map(select(length > 0))')
COUNT=$(echo "$DOCKERFILES" | jq 'length')
echo "files=$DOCKERFILES" >> $GITHUB_OUTPUT
echo "count=$COUNT" >> $GITHUB_OUTPUT
echo "Found $COUNT Dockerfiles"
- name: Install Hadolint
if: steps.find.outputs.count != '0'
run: |
wget -qO hadolint https://github.com/hadolint/hadolint/releases/download/v2.12.0/hadolint-Linux-x86_64
chmod +x hadolint
sudo mv hadolint /usr/local/bin/
- name: Lint Dockerfiles
if: steps.find.outputs.count != '0'
run: |
echo "### Dockerfile Security Lint" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
TOTAL_ISSUES=0
for dockerfile in $(echo '${{ steps.find.outputs.files }}' | jq -r '.[]'); do
echo "Linting: $dockerfile"
ISSUES=$(hadolint --format json "$dockerfile" 2>/dev/null || echo "[]")
ISSUE_COUNT=$(echo "$ISSUES" | jq 'length')
TOTAL_ISSUES=$((TOTAL_ISSUES + ISSUE_COUNT))
if [[ $ISSUE_COUNT -gt 0 ]]; then
echo "- **$dockerfile**: $ISSUE_COUNT issues" >> $GITHUB_STEP_SUMMARY
fi
done
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Total issues found: $TOTAL_ISSUES**" >> $GITHUB_STEP_SUMMARY
if [[ $TOTAL_ISSUES -gt 0 ]] && [[ "${{ github.event.inputs.fail_on_findings }}" == "true" ]]; then
echo "::warning::Found $TOTAL_ISSUES Dockerfile security issues"
fi
# ===========================================================================
# SUMMARY
# ===========================================================================
summary:
name: SAST Summary
runs-on: ubuntu-22.04
needs: [sast-scan, dotnet-security, dependency-check, dockerfile-lint]
if: always()
steps:
- name: Generate summary
run: |
echo "## SAST Scan Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Check | Status |" >> $GITHUB_STEP_SUMMARY
echo "|-------|--------|" >> $GITHUB_STEP_SUMMARY
echo "| SAST Analysis | ${{ needs.sast-scan.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| .NET Security | ${{ needs.dotnet-security.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Dependency Check | ${{ needs.dependency-check.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Dockerfile Lint | ${{ needs.dockerfile-lint.result }} |" >> $GITHUB_STEP_SUMMARY
- name: Check for failures
if: |
github.event.inputs.fail_on_findings == 'true' &&
(needs.sast-scan.result == 'failure' ||
needs.dotnet-security.result == 'failure' ||
needs.dependency-check.result == 'failure')
run: exit 1

View File

@@ -0,0 +1,105 @@
# Secrets Scanning Workflow
# Sprint: CI/CD Enhancement - Security Scanning
#
# Purpose: Detect hardcoded secrets, API keys, and credentials in code
# Triggers: Push to main/develop, all PRs
#
# Tool: PLACEHOLDER - Choose one: TruffleHog, Gitleaks, or Semgrep
name: Secrets Scanning
on:
push:
branches: [main, develop]
pull_request:
workflow_dispatch:
inputs:
scan_history:
description: 'Scan full git history'
required: false
type: boolean
default: false
jobs:
secrets-scan:
name: Scan for Secrets
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: ${{ github.event.inputs.scan_history == 'true' && 0 || 50 }}
# PLACEHOLDER: Choose your secrets scanner
# Option 1: TruffleHog (recommended - comprehensive, low false positives)
# Option 2: Gitleaks (fast, good for CI)
# Option 3: Semgrep (if already using for SAST)
- name: TruffleHog Scan
id: trufflehog
# Uncomment when ready to use TruffleHog:
# uses: trufflesecurity/trufflehog@main
# with:
# extra_args: --only-verified
run: |
echo "::notice::Secrets scanning placeholder - configure scanner below"
echo ""
echo "Available options:"
echo " 1. TruffleHog: trufflesecurity/trufflehog@main"
echo " 2. Gitleaks: gitleaks/gitleaks-action@v2"
echo " 3. Semgrep: returntocorp/semgrep-action@v1"
echo ""
echo "To enable, uncomment the appropriate action above"
# Alternative: Gitleaks
# - name: Gitleaks Scan
# uses: gitleaks/gitleaks-action@v2
# env:
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# GITLEAKS_LICENSE: ${{ secrets.GITLEAKS_LICENSE }}
# Alternative: Semgrep (secrets rules)
# - name: Semgrep Secrets Scan
# uses: returntocorp/semgrep-action@v1
# with:
# config: p/secrets
- name: Upload scan results
if: always()
uses: actions/upload-artifact@v4
with:
name: secrets-scan-results
path: |
**/trufflehog-*.json
**/gitleaks-*.json
**/semgrep-*.json
retention-days: 30
if-no-files-found: ignore
summary:
name: Scan Summary
runs-on: ubuntu-latest
needs: [secrets-scan]
if: always()
steps:
- name: Generate summary
run: |
echo "## Secrets Scanning Results" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
if [[ "${{ needs.secrets-scan.result }}" == "success" ]]; then
echo "### No secrets detected" >> $GITHUB_STEP_SUMMARY
elif [[ "${{ needs.secrets-scan.result }}" == "failure" ]]; then
echo "### Secrets detected - review required" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "Please review the scan artifacts for details." >> $GITHUB_STEP_SUMMARY
else
echo "### Scan status: ${{ needs.secrets-scan.result }}" >> $GITHUB_STEP_SUMMARY
fi
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Scanner:** Placeholder (configure in workflow)" >> $GITHUB_STEP_SUMMARY
echo "**Trigger:** ${{ github.event_name }}" >> $GITHUB_STEP_SUMMARY
echo "**Branch:** ${{ github.ref_name }}" >> $GITHUB_STEP_SUMMARY

View File

@@ -0,0 +1,490 @@
# Service Release Pipeline
# Sprint: CI/CD Enhancement - Per-Service Auto-Versioning
#
# Purpose: Automated per-service release pipeline with semantic versioning
# and Docker tag format: {semver}+{YYYYMMDDHHmmss}
#
# Triggers:
# - Tag: service-{name}-v{semver} (e.g., service-scanner-v1.2.3)
# - Manual dispatch with service selection and bump type
name: Service Release
on:
push:
tags:
- 'service-*-v*'
workflow_dispatch:
inputs:
service:
description: 'Service to release'
required: true
type: choice
options:
- authority
- attestor
- concelier
- scanner
- policy
- signer
- excititor
- gateway
- scheduler
- cli
- orchestrator
- notify
- sbomservice
- vexhub
- evidencelocker
bump_type:
description: 'Version bump type'
required: true
type: choice
options:
- patch
- minor
- major
default: 'patch'
dry_run:
description: 'Dry run (no actual release)'
required: false
type: boolean
default: false
skip_tests:
description: 'Skip tests (use with caution)'
required: false
type: boolean
default: false
env:
DOTNET_VERSION: '10.0.100'
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
DOTNET_CLI_TELEMETRY_OPTOUT: true
REGISTRY: git.stella-ops.org/stella-ops.org
SYFT_VERSION: '1.21.0'
jobs:
# ===========================================================================
# Parse tag or manual inputs to determine service and version
# ===========================================================================
resolve:
name: Resolve Release Parameters
runs-on: ubuntu-latest
outputs:
service: ${{ steps.resolve.outputs.service }}
bump_type: ${{ steps.resolve.outputs.bump_type }}
current_version: ${{ steps.resolve.outputs.current_version }}
new_version: ${{ steps.resolve.outputs.new_version }}
docker_tag: ${{ steps.resolve.outputs.docker_tag }}
is_dry_run: ${{ steps.resolve.outputs.is_dry_run }}
skip_tests: ${{ steps.resolve.outputs.skip_tests }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Resolve parameters
id: resolve
run: |
if [[ "${{ github.event_name }}" == "push" ]]; then
# Parse tag: service-{name}-v{version}
TAG="${GITHUB_REF#refs/tags/}"
echo "Processing tag: $TAG"
if [[ "$TAG" =~ ^service-([a-z]+)-v([0-9]+\.[0-9]+\.[0-9]+)$ ]]; then
SERVICE="${BASH_REMATCH[1]}"
VERSION="${BASH_REMATCH[2]}"
BUMP_TYPE="explicit"
else
echo "::error::Invalid tag format: $TAG (expected: service-{name}-v{semver})"
exit 1
fi
IS_DRY_RUN="false"
SKIP_TESTS="false"
else
# Manual dispatch
SERVICE="${{ github.event.inputs.service }}"
BUMP_TYPE="${{ github.event.inputs.bump_type }}"
VERSION="" # Will be calculated
IS_DRY_RUN="${{ github.event.inputs.dry_run }}"
SKIP_TESTS="${{ github.event.inputs.skip_tests }}"
fi
# Read current version
CURRENT_VERSION=$(.gitea/scripts/release/read-service-version.sh "$SERVICE")
echo "Current version: $CURRENT_VERSION"
# Calculate new version
if [[ -n "$VERSION" ]]; then
NEW_VERSION="$VERSION"
else
NEW_VERSION=$(python3 .gitea/scripts/release/bump-service-version.py "$SERVICE" "$BUMP_TYPE" --output-version)
fi
echo "New version: $NEW_VERSION"
# Generate Docker tag
DOCKER_TAG=$(.gitea/scripts/release/generate-docker-tag.sh --version "$NEW_VERSION")
echo "Docker tag: $DOCKER_TAG"
# Set outputs
echo "service=$SERVICE" >> $GITHUB_OUTPUT
echo "bump_type=$BUMP_TYPE" >> $GITHUB_OUTPUT
echo "current_version=$CURRENT_VERSION" >> $GITHUB_OUTPUT
echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
echo "docker_tag=$DOCKER_TAG" >> $GITHUB_OUTPUT
echo "is_dry_run=$IS_DRY_RUN" >> $GITHUB_OUTPUT
echo "skip_tests=$SKIP_TESTS" >> $GITHUB_OUTPUT
- name: Summary
run: |
echo "## Release Parameters" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Parameter | Value |" >> $GITHUB_STEP_SUMMARY
echo "|-----------|-------|" >> $GITHUB_STEP_SUMMARY
echo "| Service | ${{ steps.resolve.outputs.service }} |" >> $GITHUB_STEP_SUMMARY
echo "| Current Version | ${{ steps.resolve.outputs.current_version }} |" >> $GITHUB_STEP_SUMMARY
echo "| New Version | ${{ steps.resolve.outputs.new_version }} |" >> $GITHUB_STEP_SUMMARY
echo "| Docker Tag | ${{ steps.resolve.outputs.docker_tag }} |" >> $GITHUB_STEP_SUMMARY
echo "| Dry Run | ${{ steps.resolve.outputs.is_dry_run }} |" >> $GITHUB_STEP_SUMMARY
# ===========================================================================
# Update version in source files
# ===========================================================================
update-version:
name: Update Version
runs-on: ubuntu-latest
needs: [resolve]
if: needs.resolve.outputs.is_dry_run != 'true'
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
token: ${{ secrets.GITEA_TOKEN }}
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Update version
run: |
python3 .gitea/scripts/release/bump-service-version.py \
"${{ needs.resolve.outputs.service }}" \
"${{ needs.resolve.outputs.new_version }}" \
--docker-tag "${{ needs.resolve.outputs.docker_tag }}" \
--git-sha "${{ github.sha }}"
- name: Commit version update
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add src/Directory.Versions.props devops/releases/service-versions.json
if git diff --cached --quiet; then
echo "No version changes to commit"
else
git commit -m "chore(${{ needs.resolve.outputs.service }}): release v${{ needs.resolve.outputs.new_version }}
Docker tag: ${{ needs.resolve.outputs.docker_tag }}
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: github-actions[bot] <github-actions[bot]@users.noreply.github.com>"
git push
fi
# ===========================================================================
# Build and test the service
# ===========================================================================
build-test:
name: Build and Test
runs-on: ubuntu-latest
needs: [resolve, update-version]
if: always() && (needs.update-version.result == 'success' || needs.update-version.result == 'skipped')
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
ref: ${{ github.ref }}
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: ${{ env.DOTNET_VERSION }}
include-prerelease: true
- name: Restore dependencies
run: dotnet restore src/StellaOps.sln
- name: Build solution
run: |
dotnet build src/StellaOps.sln \
--configuration Release \
--no-restore \
-p:StellaOpsServiceVersion=${{ needs.resolve.outputs.new_version }}
- name: Run tests
if: needs.resolve.outputs.skip_tests != 'true'
run: |
SERVICE="${{ needs.resolve.outputs.service }}"
SERVICE_PASCAL=$(echo "$SERVICE" | sed -r 's/(^|-)(\w)/\U\2/g')
# Find and run tests for this service
TEST_PROJECTS=$(find src -path "*/${SERVICE_PASCAL}/*" -name "*.Tests.csproj" -o -path "*/${SERVICE_PASCAL}*Tests*" -name "*.csproj" | head -20)
if [[ -n "$TEST_PROJECTS" ]]; then
echo "Running tests for: $TEST_PROJECTS"
echo "$TEST_PROJECTS" | xargs -I{} dotnet test {} --configuration Release --no-build --verbosity normal
else
echo "::warning::No test projects found for service: $SERVICE"
fi
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: build-${{ needs.resolve.outputs.service }}
path: |
src/**/bin/Release/**/*.dll
src/**/bin/Release/**/*.exe
src/**/bin/Release/**/*.pdb
retention-days: 7
# ===========================================================================
# Build and publish Docker image
# ===========================================================================
publish-container:
name: Publish Container
runs-on: ubuntu-latest
needs: [resolve, build-test]
if: needs.resolve.outputs.is_dry_run != 'true'
outputs:
image_digest: ${{ steps.push.outputs.digest }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ secrets.REGISTRY_USERNAME }}
password: ${{ secrets.REGISTRY_PASSWORD }}
- name: Determine Dockerfile path
id: dockerfile
run: |
SERVICE="${{ needs.resolve.outputs.service }}"
SERVICE_PASCAL=$(echo "$SERVICE" | sed -r 's/(^|-)(\w)/\U\2/g')
# Look for service-specific Dockerfile
DOCKERFILE_PATHS=(
"devops/docker/${SERVICE}/Dockerfile"
"devops/docker/${SERVICE_PASCAL}/Dockerfile"
"src/${SERVICE_PASCAL}/Dockerfile"
"src/${SERVICE_PASCAL}/StellaOps.${SERVICE_PASCAL}.WebService/Dockerfile"
"devops/docker/platform/Dockerfile"
)
for path in "${DOCKERFILE_PATHS[@]}"; do
if [[ -f "$path" ]]; then
echo "dockerfile=$path" >> $GITHUB_OUTPUT
echo "Found Dockerfile: $path"
exit 0
fi
done
echo "::error::No Dockerfile found for service: $SERVICE"
exit 1
- name: Build and push image
id: push
uses: docker/build-push-action@v5
with:
context: .
file: ${{ steps.dockerfile.outputs.dockerfile }}
push: true
tags: |
${{ env.REGISTRY }}/${{ needs.resolve.outputs.service }}:${{ needs.resolve.outputs.docker_tag }}
${{ env.REGISTRY }}/${{ needs.resolve.outputs.service }}:${{ needs.resolve.outputs.new_version }}
${{ env.REGISTRY }}/${{ needs.resolve.outputs.service }}:latest
labels: |
org.opencontainers.image.title=${{ needs.resolve.outputs.service }}
org.opencontainers.image.version=${{ needs.resolve.outputs.new_version }}
org.opencontainers.image.revision=${{ github.sha }}
org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }}
com.stellaops.service.name=${{ needs.resolve.outputs.service }}
com.stellaops.service.version=${{ needs.resolve.outputs.new_version }}
com.stellaops.docker.tag=${{ needs.resolve.outputs.docker_tag }}
build-args: |
VERSION=${{ needs.resolve.outputs.new_version }}
GIT_SHA=${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Image summary
run: |
echo "## Container Image" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Property | Value |" >> $GITHUB_STEP_SUMMARY
echo "|----------|-------|" >> $GITHUB_STEP_SUMMARY
echo "| Image | \`${{ env.REGISTRY }}/${{ needs.resolve.outputs.service }}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Tag | \`${{ needs.resolve.outputs.docker_tag }}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Digest | \`${{ steps.push.outputs.digest }}\` |" >> $GITHUB_STEP_SUMMARY
# ===========================================================================
# Generate SBOM
# ===========================================================================
generate-sbom:
name: Generate SBOM
runs-on: ubuntu-latest
needs: [resolve, publish-container]
if: needs.resolve.outputs.is_dry_run != 'true'
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install Syft
run: |
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | \
sh -s -- -b /usr/local/bin v${{ env.SYFT_VERSION }}
- name: Login to registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ secrets.REGISTRY_USERNAME }}
password: ${{ secrets.REGISTRY_PASSWORD }}
- name: Generate SBOM
run: |
IMAGE="${{ env.REGISTRY }}/${{ needs.resolve.outputs.service }}:${{ needs.resolve.outputs.docker_tag }}"
syft "$IMAGE" \
--output cyclonedx-json=sbom.cyclonedx.json \
--output spdx-json=sbom.spdx.json
echo "Generated SBOMs for: $IMAGE"
- name: Upload SBOM artifacts
uses: actions/upload-artifact@v4
with:
name: sbom-${{ needs.resolve.outputs.service }}-${{ needs.resolve.outputs.new_version }}
path: |
sbom.cyclonedx.json
sbom.spdx.json
retention-days: 90
# ===========================================================================
# Sign artifacts with Cosign
# ===========================================================================
sign-artifacts:
name: Sign Artifacts
runs-on: ubuntu-latest
needs: [resolve, publish-container, generate-sbom]
if: needs.resolve.outputs.is_dry_run != 'true'
steps:
- name: Install Cosign
uses: sigstore/cosign-installer@v3
- name: Login to registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ secrets.REGISTRY_USERNAME }}
password: ${{ secrets.REGISTRY_PASSWORD }}
- name: Sign container image
if: env.COSIGN_PRIVATE_KEY_B64 != ''
env:
COSIGN_PRIVATE_KEY_B64: ${{ secrets.COSIGN_PRIVATE_KEY_B64 }}
COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }}
run: |
echo "$COSIGN_PRIVATE_KEY_B64" | base64 -d > cosign.key
IMAGE="${{ env.REGISTRY }}/${{ needs.resolve.outputs.service }}@${{ needs.publish-container.outputs.image_digest }}"
cosign sign --key cosign.key \
-a "service=${{ needs.resolve.outputs.service }}" \
-a "version=${{ needs.resolve.outputs.new_version }}" \
-a "docker-tag=${{ needs.resolve.outputs.docker_tag }}" \
"$IMAGE"
rm -f cosign.key
echo "Signed: $IMAGE"
- name: Download SBOM
uses: actions/download-artifact@v4
with:
name: sbom-${{ needs.resolve.outputs.service }}-${{ needs.resolve.outputs.new_version }}
path: sbom/
- name: Attach SBOM to image
if: env.COSIGN_PRIVATE_KEY_B64 != ''
env:
COSIGN_PRIVATE_KEY_B64: ${{ secrets.COSIGN_PRIVATE_KEY_B64 }}
COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }}
run: |
echo "$COSIGN_PRIVATE_KEY_B64" | base64 -d > cosign.key
IMAGE="${{ env.REGISTRY }}/${{ needs.resolve.outputs.service }}@${{ needs.publish-container.outputs.image_digest }}"
cosign attach sbom --sbom sbom/sbom.cyclonedx.json "$IMAGE"
cosign sign --key cosign.key --attachment sbom "$IMAGE"
rm -f cosign.key
# ===========================================================================
# Release summary
# ===========================================================================
summary:
name: Release Summary
runs-on: ubuntu-latest
needs: [resolve, build-test, publish-container, generate-sbom, sign-artifacts]
if: always()
steps:
- name: Generate summary
run: |
echo "# Service Release: ${{ needs.resolve.outputs.service }}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "## Release Details" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Property | Value |" >> $GITHUB_STEP_SUMMARY
echo "|----------|-------|" >> $GITHUB_STEP_SUMMARY
echo "| Service | ${{ needs.resolve.outputs.service }} |" >> $GITHUB_STEP_SUMMARY
echo "| Version | ${{ needs.resolve.outputs.new_version }} |" >> $GITHUB_STEP_SUMMARY
echo "| Previous | ${{ needs.resolve.outputs.current_version }} |" >> $GITHUB_STEP_SUMMARY
echo "| Docker Tag | \`${{ needs.resolve.outputs.docker_tag }}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Git SHA | \`${{ github.sha }}\` |" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "## Job Results" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Job | Status |" >> $GITHUB_STEP_SUMMARY
echo "|-----|--------|" >> $GITHUB_STEP_SUMMARY
echo "| Build & Test | ${{ needs.build-test.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Publish Container | ${{ needs.publish-container.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Generate SBOM | ${{ needs.generate-sbom.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Sign Artifacts | ${{ needs.sign-artifacts.result }} |" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
if [[ "${{ needs.resolve.outputs.is_dry_run }}" == "true" ]]; then
echo "⚠️ **This was a dry run. No artifacts were published.**" >> $GITHUB_STEP_SUMMARY
else
echo "## Pull Image" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "\`\`\`bash" >> $GITHUB_STEP_SUMMARY
echo "docker pull ${{ env.REGISTRY }}/${{ needs.resolve.outputs.service }}:${{ needs.resolve.outputs.docker_tag }}" >> $GITHUB_STEP_SUMMARY
echo "\`\`\`" >> $GITHUB_STEP_SUMMARY
fi

View File

@@ -0,0 +1,267 @@
# =============================================================================
# replay-verify.yml
# Sprint: SPRINT_20251228_001_BE_replay_manifest_ci (T4)
# Description: CI workflow template for SBOM hash drift detection
# =============================================================================
#
# This workflow verifies that SBOM generation and verdict computation are
# deterministic by comparing replay manifest hashes across builds.
#
# Usage:
# 1. Copy this template to your project's .gitea/workflows/ directory
# 2. Adjust the image name and scan parameters as needed
# 3. Optionally enable the SBOM attestation step
#
# Exit codes:
# 0 - Verification passed, all hashes match
# 1 - Drift detected, hashes differ
# 2 - Verification error (missing inputs, invalid manifest)
#
# =============================================================================
name: SBOM Replay Verification
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
workflow_dispatch:
inputs:
fail_on_drift:
description: 'Fail build if hash drift detected'
required: false
default: 'true'
type: boolean
strict_mode:
description: 'Enable strict verification mode'
required: false
default: 'false'
type: boolean
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
STELLAOPS_VERSION: '1.0.0'
jobs:
build-and-scan:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
id-token: write # For OIDC-based signing
outputs:
image_digest: ${{ steps.build.outputs.digest }}
sbom_digest: ${{ steps.scan.outputs.sbom_digest }}
verdict_digest: ${{ steps.scan.outputs.verdict_digest }}
replay_manifest: ${{ steps.scan.outputs.replay_manifest }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to container registry
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=sha,prefix=
type=ref,event=branch
type=ref,event=pr
- name: Build and push image
id: build
uses: docker/build-push-action@v5
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
provenance: true
sbom: false # We generate our own SBOM
- name: Install StellaOps CLI
run: |
curl -sSfL https://stellaops.io/install.sh | sh -s -- -v ${{ env.STELLAOPS_VERSION }}
echo "$HOME/.stellaops/bin" >> $GITHUB_PATH
- name: Scan image and generate replay manifest
id: scan
env:
IMAGE_REF: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build.outputs.digest }}
run: |
# Scan image with StellaOps
stella scan \
--image "${IMAGE_REF}" \
--output-sbom sbom.json \
--output-findings findings.json \
--output-verdict verdict.json \
--format cyclonedx-1.6
# Export replay manifest for CI verification
stella replay export \
--image "${IMAGE_REF}" \
--output replay.json \
--include-feeds \
--include-reachability \
--pretty
# Extract digests for outputs
SBOM_DIGEST=$(sha256sum sbom.json | cut -d' ' -f1)
VERDICT_DIGEST=$(sha256sum verdict.json | cut -d' ' -f1)
echo "sbom_digest=sha256:${SBOM_DIGEST}" >> $GITHUB_OUTPUT
echo "verdict_digest=sha256:${VERDICT_DIGEST}" >> $GITHUB_OUTPUT
echo "replay_manifest=replay.json" >> $GITHUB_OUTPUT
# Display summary
echo "### Scan Results" >> $GITHUB_STEP_SUMMARY
echo "| Artifact | Digest |" >> $GITHUB_STEP_SUMMARY
echo "|----------|--------|" >> $GITHUB_STEP_SUMMARY
echo "| Image | \`${{ steps.build.outputs.digest }}\` |" >> $GITHUB_STEP_SUMMARY
echo "| SBOM | \`sha256:${SBOM_DIGEST}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Verdict | \`sha256:${VERDICT_DIGEST}\` |" >> $GITHUB_STEP_SUMMARY
- name: Upload scan artifacts
uses: actions/upload-artifact@v4
with:
name: scan-artifacts-${{ github.sha }}
path: |
sbom.json
findings.json
verdict.json
replay.json
retention-days: 30
verify-determinism:
runs-on: ubuntu-latest
needs: build-and-scan
steps:
- name: Download scan artifacts
uses: actions/download-artifact@v4
with:
name: scan-artifacts-${{ github.sha }}
- name: Install StellaOps CLI
run: |
curl -sSfL https://stellaops.io/install.sh | sh -s -- -v ${{ env.STELLAOPS_VERSION }}
echo "$HOME/.stellaops/bin" >> $GITHUB_PATH
- name: Verify SBOM determinism
id: verify
env:
FAIL_ON_DRIFT: ${{ inputs.fail_on_drift || 'true' }}
STRICT_MODE: ${{ inputs.strict_mode || 'false' }}
run: |
# Build verification flags
VERIFY_FLAGS="--manifest replay.json"
if [ "${FAIL_ON_DRIFT}" = "true" ]; then
VERIFY_FLAGS="${VERIFY_FLAGS} --fail-on-drift"
fi
if [ "${STRICT_MODE}" = "true" ]; then
VERIFY_FLAGS="${VERIFY_FLAGS} --strict-mode"
fi
# Run verification
stella replay export verify ${VERIFY_FLAGS}
EXIT_CODE=$?
# Report results
if [ $EXIT_CODE -eq 0 ]; then
echo "✅ Verification passed - all hashes match" >> $GITHUB_STEP_SUMMARY
echo "status=success" >> $GITHUB_OUTPUT
elif [ $EXIT_CODE -eq 1 ]; then
echo "⚠️ Drift detected - hashes differ from expected" >> $GITHUB_STEP_SUMMARY
echo "status=drift" >> $GITHUB_OUTPUT
else
echo "❌ Verification error" >> $GITHUB_STEP_SUMMARY
echo "status=error" >> $GITHUB_OUTPUT
fi
exit $EXIT_CODE
- name: Comment on PR (on drift)
if: failure() && github.event_name == 'pull_request'
uses: actions/github-script@v7
with:
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `## ⚠️ SBOM Determinism Check Failed
Hash drift detected between scan runs. This may indicate non-deterministic build or scan behavior.
**Expected digests:**
- SBOM: \`${{ needs.build-and-scan.outputs.sbom_digest }}\`
- Verdict: \`${{ needs.build-and-scan.outputs.verdict_digest }}\`
**Possible causes:**
- Non-deterministic build artifacts (timestamps, random values)
- Changed dependencies between runs
- Environment differences
**Next steps:**
1. Review the replay manifest in the artifacts
2. Check build logs for non-deterministic elements
3. Consider using \`--strict-mode\` for detailed drift analysis`
})
# Optional: Attest SBOM to OCI registry
attest-sbom:
runs-on: ubuntu-latest
needs: [build-and-scan, verify-determinism]
if: github.event_name != 'pull_request' && success()
permissions:
packages: write
id-token: write
steps:
- name: Download scan artifacts
uses: actions/download-artifact@v4
with:
name: scan-artifacts-${{ github.sha }}
- name: Install StellaOps CLI
run: |
curl -sSfL https://stellaops.io/install.sh | sh -s -- -v ${{ env.STELLAOPS_VERSION }}
echo "$HOME/.stellaops/bin" >> $GITHUB_PATH
- name: Log in to container registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Attach SBOM attestation
env:
IMAGE_REF: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ needs.build-and-scan.outputs.image_digest }}
run: |
# Sign and attach SBOM as in-toto attestation
stella attest attach \
--image "${IMAGE_REF}" \
--sbom sbom.json \
--predicate-type https://cyclonedx.org/bom/v1.6 \
--sign keyless
echo "### SBOM Attestation" >> $GITHUB_STEP_SUMMARY
echo "SBOM attached to \`${IMAGE_REF}\`" >> $GITHUB_STEP_SUMMARY

View File

@@ -1,9 +1,10 @@
# .gitea/workflows/test-matrix.yml
# Unified test matrix pipeline with TRX reporting for all test categories
# Sprint: SPRINT_20251226_007_CICD - Dynamic test discovery
# Refactored: SPRINT_CICD_Enhancement - DRY principle, matrix strategy
#
# WORKFLOW INTEGRATION STRATEGY (Sprint 20251226_003_CICD):
# =========================================================
# WORKFLOW INTEGRATION STRATEGY:
# ==============================
# This workflow is the PRIMARY test execution workflow for PR gating.
# It dynamically discovers and runs ALL test projects by Category trait.
#
@@ -12,8 +13,6 @@
#
# Scheduled/On-Demand Categories:
# Performance, Benchmark, AirGap, Chaos, Determinism, Resilience, Observability
#
# For build/deploy operations, see: build-test-deploy.yml (runs in parallel)
name: Test Matrix
@@ -85,10 +84,6 @@ jobs:
- name: Find all test projects
id: find
run: |
# Find all test project files, including non-standard naming conventions:
# - *.Tests.csproj (standard)
# - *UnitTests.csproj, *SmokeTests.csproj, *FixtureTests.csproj, *IntegrationTests.csproj
# Exclude: TestKit, Testing libraries, node_modules, bin, obj
PROJECTS=$(find src \( \
-name "*.Tests.csproj" \
-o -name "*UnitTests.csproj" \
@@ -104,11 +99,9 @@ jobs:
! -name "*Testing.csproj" \
| sort)
# Count projects
COUNT=$(echo "$PROJECTS" | grep -c '.csproj' || echo "0")
echo "Found $COUNT test projects"
# Output as JSON array for matrix
echo "projects=$(echo "$PROJECTS" | jq -R -s -c 'split("\n") | map(select(length > 0))')" >> $GITHUB_OUTPUT
echo "count=$COUNT" >> $GITHUB_OUTPUT
@@ -122,13 +115,34 @@ jobs:
# ===========================================================================
# PR-GATING TESTS (run on every push/PR)
# Uses matrix strategy to run all categories in parallel
# ===========================================================================
unit:
name: Unit Tests
pr-gating-tests:
name: ${{ matrix.category }} Tests
runs-on: ubuntu-22.04
timeout-minutes: 20
timeout-minutes: ${{ matrix.timeout }}
needs: discover
strategy:
fail-fast: false
matrix:
include:
- category: Unit
timeout: 20
collect_coverage: true
- category: Architecture
timeout: 15
collect_coverage: false
- category: Contract
timeout: 15
collect_coverage: false
- category: Security
timeout: 25
collect_coverage: false
- category: Golden
timeout: 25
collect_coverage: false
steps:
- name: Checkout
uses: actions/checkout@v4
@@ -141,165 +155,26 @@ jobs:
dotnet-version: ${{ env.DOTNET_VERSION }}
include-prerelease: true
- name: Run Unit Tests (all test projects)
- name: Run ${{ matrix.category }} Tests
run: |
mkdir -p ./TestResults/Unit
FAILED=0
PASSED=0
SKIPPED=0
# Find and run all test projects with Unit category
# Use expanded pattern to include non-standard naming conventions
for proj in $(find src \( -name "*.Tests.csproj" -o -name "*UnitTests.csproj" -o -name "*SmokeTests.csproj" -o -name "*FixtureTests.csproj" -o -name "*IntegrationTests.csproj" \) -type f ! -path "*/node_modules/*" ! -name "StellaOps.TestKit.csproj" ! -name "*Testing.csproj" | sort); do
echo "::group::Testing $proj"
# Create unique TRX filename using path hash to avoid duplicates
TRX_NAME=$(echo "$proj" | sed 's|/|_|g' | sed 's|\.csproj||')-unit.trx
# Restore and build in one step, then test
if dotnet test "$proj" \
--filter "Category=Unit" \
--configuration Release \
--logger "trx;LogFileName=$TRX_NAME" \
--results-directory ./TestResults/Unit \
--collect:"XPlat Code Coverage" \
--verbosity minimal 2>&1; then
PASSED=$((PASSED + 1))
echo "✓ $proj passed"
else
# Check if it was just "no tests matched" which is not a failure
if [ $? -eq 0 ] || grep -q "No test matches" /tmp/test-output.txt 2>/dev/null; then
SKIPPED=$((SKIPPED + 1))
echo "○ $proj skipped (no Unit tests)"
else
FAILED=$((FAILED + 1))
echo "✗ $proj failed"
fi
fi
echo "::endgroup::"
done
echo "## Unit Test Summary" >> $GITHUB_STEP_SUMMARY
echo "- Passed: $PASSED" >> $GITHUB_STEP_SUMMARY
echo "- Failed: $FAILED" >> $GITHUB_STEP_SUMMARY
echo "- Skipped: $SKIPPED" >> $GITHUB_STEP_SUMMARY
# Fail if any tests failed
if [ $FAILED -gt 0 ]; then
exit 1
chmod +x .gitea/scripts/test/run-test-category.sh
if [[ "${{ matrix.collect_coverage }}" == "true" ]]; then
.gitea/scripts/test/run-test-category.sh "${{ matrix.category }}" --collect-coverage
else
.gitea/scripts/test/run-test-category.sh "${{ matrix.category }}"
fi
- name: Upload Test Results
uses: actions/upload-artifact@v4
if: always()
with:
name: test-results-unit
path: ./TestResults/Unit
name: test-results-${{ matrix.category }}
path: ./TestResults/${{ matrix.category }}
retention-days: 14
architecture:
name: Architecture Tests
runs-on: ubuntu-22.04
timeout-minutes: 15
needs: discover
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 }}
include-prerelease: true
- name: Run Architecture Tests (all test projects)
run: |
mkdir -p ./TestResults/Architecture
FAILED=0
PASSED=0
SKIPPED=0
for proj in $(find src \( -name "*.Tests.csproj" -o -name "*UnitTests.csproj" -o -name "*SmokeTests.csproj" -o -name "*FixtureTests.csproj" -o -name "*IntegrationTests.csproj" \) -type f ! -path "*/node_modules/*" ! -name "StellaOps.TestKit.csproj" ! -name "*Testing.csproj" | sort); do
echo "::group::Testing $proj"
TRX_NAME=$(echo "$proj" | sed 's|/|_|g' | sed 's|\.csproj||')-architecture.trx
if dotnet test "$proj" \
--filter "Category=Architecture" \
--configuration Release \
--logger "trx;LogFileName=$TRX_NAME" \
--results-directory ./TestResults/Architecture \
--verbosity minimal 2>&1; then
PASSED=$((PASSED + 1))
else
SKIPPED=$((SKIPPED + 1))
fi
echo "::endgroup::"
done
echo "## Architecture Test Summary" >> $GITHUB_STEP_SUMMARY
echo "- Passed: $PASSED" >> $GITHUB_STEP_SUMMARY
echo "- Skipped: $SKIPPED" >> $GITHUB_STEP_SUMMARY
- name: Upload Test Results
uses: actions/upload-artifact@v4
if: always()
with:
name: test-results-architecture
path: ./TestResults/Architecture
retention-days: 14
contract:
name: Contract Tests
runs-on: ubuntu-22.04
timeout-minutes: 15
needs: discover
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 }}
include-prerelease: true
- name: Run Contract Tests (all test projects)
run: |
mkdir -p ./TestResults/Contract
FAILED=0
PASSED=0
SKIPPED=0
for proj in $(find src \( -name "*.Tests.csproj" -o -name "*UnitTests.csproj" -o -name "*SmokeTests.csproj" -o -name "*FixtureTests.csproj" -o -name "*IntegrationTests.csproj" \) -type f ! -path "*/node_modules/*" ! -name "StellaOps.TestKit.csproj" ! -name "*Testing.csproj" | sort); do
echo "::group::Testing $proj"
TRX_NAME=$(echo "$proj" | sed 's|/|_|g' | sed 's|\.csproj||')-contract.trx
if dotnet test "$proj" \
--filter "Category=Contract" \
--configuration Release \
--logger "trx;LogFileName=$TRX_NAME" \
--results-directory ./TestResults/Contract \
--verbosity minimal 2>&1; then
PASSED=$((PASSED + 1))
else
SKIPPED=$((SKIPPED + 1))
fi
echo "::endgroup::"
done
echo "## Contract Test Summary" >> $GITHUB_STEP_SUMMARY
echo "- Passed: $PASSED" >> $GITHUB_STEP_SUMMARY
echo "- Skipped: $SKIPPED" >> $GITHUB_STEP_SUMMARY
- name: Upload Test Results
uses: actions/upload-artifact@v4
if: always()
with:
name: test-results-contract
path: ./TestResults/Contract
retention-days: 14
# ===========================================================================
# INTEGRATION TESTS (separate due to service dependency)
# ===========================================================================
integration:
name: Integration Tests
@@ -332,520 +207,112 @@ jobs:
dotnet-version: ${{ env.DOTNET_VERSION }}
include-prerelease: true
- name: Run Integration Tests (all test projects)
- name: Run Integration Tests
env:
STELLAOPS_TEST_POSTGRES_CONNECTION: "Host=localhost;Port=5432;Database=stellaops_test;Username=stellaops;Password=stellaops"
run: |
mkdir -p ./TestResults/Integration
FAILED=0
PASSED=0
SKIPPED=0
for proj in $(find src \( -name "*.Tests.csproj" -o -name "*UnitTests.csproj" -o -name "*SmokeTests.csproj" -o -name "*FixtureTests.csproj" -o -name "*IntegrationTests.csproj" \) -type f ! -path "*/node_modules/*" ! -name "StellaOps.TestKit.csproj" ! -name "*Testing.csproj" | sort); do
echo "::group::Testing $proj"
TRX_NAME=$(echo "$proj" | sed 's|/|_|g' | sed 's|\.csproj||')-integration.trx
if dotnet test "$proj" \
--filter "Category=Integration" \
--configuration Release \
--logger "trx;LogFileName=$TRX_NAME" \
--results-directory ./TestResults/Integration \
--verbosity minimal 2>&1; then
PASSED=$((PASSED + 1))
else
SKIPPED=$((SKIPPED + 1))
fi
echo "::endgroup::"
done
echo "## Integration Test Summary" >> $GITHUB_STEP_SUMMARY
echo "- Passed: $PASSED" >> $GITHUB_STEP_SUMMARY
echo "- Skipped: $SKIPPED" >> $GITHUB_STEP_SUMMARY
chmod +x .gitea/scripts/test/run-test-category.sh
.gitea/scripts/test/run-test-category.sh Integration
- name: Upload Test Results
uses: actions/upload-artifact@v4
if: always()
with:
name: test-results-integration
name: test-results-Integration
path: ./TestResults/Integration
retention-days: 14
security:
name: Security Tests
runs-on: ubuntu-22.04
timeout-minutes: 25
needs: discover
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 }}
include-prerelease: true
- name: Run Security Tests (all test projects)
run: |
mkdir -p ./TestResults/Security
FAILED=0
PASSED=0
SKIPPED=0
for proj in $(find src \( -name "*.Tests.csproj" -o -name "*UnitTests.csproj" -o -name "*SmokeTests.csproj" -o -name "*FixtureTests.csproj" -o -name "*IntegrationTests.csproj" \) -type f ! -path "*/node_modules/*" ! -name "StellaOps.TestKit.csproj" ! -name "*Testing.csproj" | sort); do
echo "::group::Testing $proj"
TRX_NAME=$(echo "$proj" | sed 's|/|_|g' | sed 's|\.csproj||')-security.trx
if dotnet test "$proj" \
--filter "Category=Security" \
--configuration Release \
--logger "trx;LogFileName=$TRX_NAME" \
--results-directory ./TestResults/Security \
--verbosity minimal 2>&1; then
PASSED=$((PASSED + 1))
else
SKIPPED=$((SKIPPED + 1))
fi
echo "::endgroup::"
done
echo "## Security Test Summary" >> $GITHUB_STEP_SUMMARY
echo "- Passed: $PASSED" >> $GITHUB_STEP_SUMMARY
echo "- Skipped: $SKIPPED" >> $GITHUB_STEP_SUMMARY
- name: Upload Test Results
uses: actions/upload-artifact@v4
if: always()
with:
name: test-results-security
path: ./TestResults/Security
retention-days: 14
golden:
name: Golden Tests
runs-on: ubuntu-22.04
timeout-minutes: 25
needs: discover
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 }}
include-prerelease: true
- name: Run Golden Tests (all test projects)
run: |
mkdir -p ./TestResults/Golden
FAILED=0
PASSED=0
SKIPPED=0
for proj in $(find src \( -name "*.Tests.csproj" -o -name "*UnitTests.csproj" -o -name "*SmokeTests.csproj" -o -name "*FixtureTests.csproj" -o -name "*IntegrationTests.csproj" \) -type f ! -path "*/node_modules/*" ! -name "StellaOps.TestKit.csproj" ! -name "*Testing.csproj" | sort); do
echo "::group::Testing $proj"
TRX_NAME=$(echo "$proj" | sed 's|/|_|g' | sed 's|\.csproj||')-golden.trx
if dotnet test "$proj" \
--filter "Category=Golden" \
--configuration Release \
--logger "trx;LogFileName=$TRX_NAME" \
--results-directory ./TestResults/Golden \
--verbosity minimal 2>&1; then
PASSED=$((PASSED + 1))
else
SKIPPED=$((SKIPPED + 1))
fi
echo "::endgroup::"
done
echo "## Golden Test Summary" >> $GITHUB_STEP_SUMMARY
echo "- Passed: $PASSED" >> $GITHUB_STEP_SUMMARY
echo "- Skipped: $SKIPPED" >> $GITHUB_STEP_SUMMARY
- name: Upload Test Results
uses: actions/upload-artifact@v4
if: always()
with:
name: test-results-golden
path: ./TestResults/Golden
retention-days: 14
# ===========================================================================
# SCHEDULED/ON-DEMAND TESTS
# Uses matrix strategy for extended test categories
# ===========================================================================
performance:
name: Performance Tests
extended-tests:
name: ${{ matrix.category }} Tests
runs-on: ubuntu-22.04
timeout-minutes: 45
timeout-minutes: ${{ matrix.timeout }}
needs: discover
if: github.event_name == 'schedule' || github.event.inputs.include_performance == 'true'
if: >-
github.event_name == 'schedule' ||
github.event.inputs.include_performance == 'true' ||
github.event.inputs.include_benchmark == 'true' ||
github.event.inputs.include_airgap == 'true' ||
github.event.inputs.include_chaos == 'true' ||
github.event.inputs.include_determinism == 'true' ||
github.event.inputs.include_resilience == 'true' ||
github.event.inputs.include_observability == 'true'
strategy:
fail-fast: false
matrix:
include:
- category: Performance
timeout: 45
trigger_input: include_performance
run_on_schedule: true
- category: Benchmark
timeout: 60
trigger_input: include_benchmark
run_on_schedule: true
- category: AirGap
timeout: 45
trigger_input: include_airgap
run_on_schedule: false
- category: Chaos
timeout: 45
trigger_input: include_chaos
run_on_schedule: false
- category: Determinism
timeout: 45
trigger_input: include_determinism
run_on_schedule: false
- category: Resilience
timeout: 45
trigger_input: include_resilience
run_on_schedule: false
- category: Observability
timeout: 30
trigger_input: include_observability
run_on_schedule: false
steps:
- name: Check if should run
id: should_run
run: |
SHOULD_RUN="false"
if [[ "${{ github.event_name }}" == "schedule" && "${{ matrix.run_on_schedule }}" == "true" ]]; then
SHOULD_RUN="true"
fi
if [[ "${{ github.event.inputs[matrix.trigger_input] }}" == "true" ]]; then
SHOULD_RUN="true"
fi
echo "run=$SHOULD_RUN" >> $GITHUB_OUTPUT
echo "Should run ${{ matrix.category }}: $SHOULD_RUN"
- name: Checkout
if: steps.should_run.outputs.run == 'true'
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup .NET
if: steps.should_run.outputs.run == 'true'
uses: actions/setup-dotnet@v4
with:
dotnet-version: ${{ env.DOTNET_VERSION }}
include-prerelease: true
- name: Run Performance Tests (all test projects)
- name: Run ${{ matrix.category }} Tests
if: steps.should_run.outputs.run == 'true'
run: |
mkdir -p ./TestResults/Performance
FAILED=0
PASSED=0
SKIPPED=0
for proj in $(find src \( -name "*.Tests.csproj" -o -name "*UnitTests.csproj" -o -name "*SmokeTests.csproj" -o -name "*FixtureTests.csproj" -o -name "*IntegrationTests.csproj" \) -type f ! -path "*/node_modules/*" ! -name "StellaOps.TestKit.csproj" ! -name "*Testing.csproj" | sort); do
echo "::group::Testing $proj"
TRX_NAME=$(echo "$proj" | sed 's|/|_|g' | sed 's|\.csproj||')-performance.trx
if dotnet test "$proj" \
--filter "Category=Performance" \
--configuration Release \
--logger "trx;LogFileName=$TRX_NAME" \
--results-directory ./TestResults/Performance \
--verbosity minimal 2>&1; then
PASSED=$((PASSED + 1))
else
SKIPPED=$((SKIPPED + 1))
fi
echo "::endgroup::"
done
echo "## Performance Test Summary" >> $GITHUB_STEP_SUMMARY
echo "- Passed: $PASSED" >> $GITHUB_STEP_SUMMARY
echo "- Skipped: $SKIPPED" >> $GITHUB_STEP_SUMMARY
chmod +x .gitea/scripts/test/run-test-category.sh
.gitea/scripts/test/run-test-category.sh "${{ matrix.category }}"
- name: Upload Test Results
uses: actions/upload-artifact@v4
if: always()
if: always() && steps.should_run.outputs.run == 'true'
with:
name: test-results-performance
path: ./TestResults/Performance
retention-days: 14
benchmark:
name: Benchmark Tests
runs-on: ubuntu-22.04
timeout-minutes: 60
needs: discover
if: github.event_name == 'schedule' || github.event.inputs.include_benchmark == 'true'
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 }}
include-prerelease: true
- name: Run Benchmark Tests (all test projects)
run: |
mkdir -p ./TestResults/Benchmark
FAILED=0
PASSED=0
SKIPPED=0
for proj in $(find src \( -name "*.Tests.csproj" -o -name "*UnitTests.csproj" -o -name "*SmokeTests.csproj" -o -name "*FixtureTests.csproj" -o -name "*IntegrationTests.csproj" \) -type f ! -path "*/node_modules/*" ! -name "StellaOps.TestKit.csproj" ! -name "*Testing.csproj" | sort); do
echo "::group::Testing $proj"
TRX_NAME=$(echo "$proj" | sed 's|/|_|g' | sed 's|\.csproj||')-benchmark.trx
if dotnet test "$proj" \
--filter "Category=Benchmark" \
--configuration Release \
--logger "trx;LogFileName=$TRX_NAME" \
--results-directory ./TestResults/Benchmark \
--verbosity minimal 2>&1; then
PASSED=$((PASSED + 1))
else
SKIPPED=$((SKIPPED + 1))
fi
echo "::endgroup::"
done
echo "## Benchmark Test Summary" >> $GITHUB_STEP_SUMMARY
echo "- Passed: $PASSED" >> $GITHUB_STEP_SUMMARY
echo "- Skipped: $SKIPPED" >> $GITHUB_STEP_SUMMARY
- name: Upload Test Results
uses: actions/upload-artifact@v4
if: always()
with:
name: test-results-benchmark
path: ./TestResults/Benchmark
retention-days: 14
airgap:
name: AirGap Tests
runs-on: ubuntu-22.04
timeout-minutes: 45
needs: discover
if: github.event.inputs.include_airgap == 'true'
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 }}
include-prerelease: true
- name: Run AirGap Tests (all test projects)
run: |
mkdir -p ./TestResults/AirGap
FAILED=0
PASSED=0
SKIPPED=0
for proj in $(find src \( -name "*.Tests.csproj" -o -name "*UnitTests.csproj" -o -name "*SmokeTests.csproj" -o -name "*FixtureTests.csproj" -o -name "*IntegrationTests.csproj" \) -type f ! -path "*/node_modules/*" ! -name "StellaOps.TestKit.csproj" ! -name "*Testing.csproj" | sort); do
echo "::group::Testing $proj"
TRX_NAME=$(echo "$proj" | sed 's|/|_|g' | sed 's|\.csproj||')-airgap.trx
if dotnet test "$proj" \
--filter "Category=AirGap" \
--configuration Release \
--logger "trx;LogFileName=$TRX_NAME" \
--results-directory ./TestResults/AirGap \
--verbosity minimal 2>&1; then
PASSED=$((PASSED + 1))
else
SKIPPED=$((SKIPPED + 1))
fi
echo "::endgroup::"
done
echo "## AirGap Test Summary" >> $GITHUB_STEP_SUMMARY
echo "- Passed: $PASSED" >> $GITHUB_STEP_SUMMARY
echo "- Skipped: $SKIPPED" >> $GITHUB_STEP_SUMMARY
- name: Upload Test Results
uses: actions/upload-artifact@v4
if: always()
with:
name: test-results-airgap
path: ./TestResults/AirGap
retention-days: 14
chaos:
name: Chaos Tests
runs-on: ubuntu-22.04
timeout-minutes: 45
needs: discover
if: github.event.inputs.include_chaos == 'true'
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 }}
include-prerelease: true
- name: Run Chaos Tests (all test projects)
run: |
mkdir -p ./TestResults/Chaos
FAILED=0
PASSED=0
SKIPPED=0
for proj in $(find src \( -name "*.Tests.csproj" -o -name "*UnitTests.csproj" -o -name "*SmokeTests.csproj" -o -name "*FixtureTests.csproj" -o -name "*IntegrationTests.csproj" \) -type f ! -path "*/node_modules/*" ! -name "StellaOps.TestKit.csproj" ! -name "*Testing.csproj" | sort); do
echo "::group::Testing $proj"
TRX_NAME=$(echo "$proj" | sed 's|/|_|g' | sed 's|\.csproj||')-chaos.trx
if dotnet test "$proj" \
--filter "Category=Chaos" \
--configuration Release \
--logger "trx;LogFileName=$TRX_NAME" \
--results-directory ./TestResults/Chaos \
--verbosity minimal 2>&1; then
PASSED=$((PASSED + 1))
else
SKIPPED=$((SKIPPED + 1))
fi
echo "::endgroup::"
done
echo "## Chaos Test Summary" >> $GITHUB_STEP_SUMMARY
echo "- Passed: $PASSED" >> $GITHUB_STEP_SUMMARY
echo "- Skipped: $SKIPPED" >> $GITHUB_STEP_SUMMARY
- name: Upload Test Results
uses: actions/upload-artifact@v4
if: always()
with:
name: test-results-chaos
path: ./TestResults/Chaos
retention-days: 14
determinism:
name: Determinism Tests
runs-on: ubuntu-22.04
timeout-minutes: 45
needs: discover
if: github.event.inputs.include_determinism == 'true'
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 }}
include-prerelease: true
- name: Run Determinism Tests (all test projects)
run: |
mkdir -p ./TestResults/Determinism
FAILED=0
PASSED=0
SKIPPED=0
for proj in $(find src \( -name "*.Tests.csproj" -o -name "*UnitTests.csproj" -o -name "*SmokeTests.csproj" -o -name "*FixtureTests.csproj" -o -name "*IntegrationTests.csproj" \) -type f ! -path "*/node_modules/*" ! -name "StellaOps.TestKit.csproj" ! -name "*Testing.csproj" | sort); do
echo "::group::Testing $proj"
TRX_NAME=$(echo "$proj" | sed 's|/|_|g' | sed 's|\.csproj||')-determinism.trx
if dotnet test "$proj" \
--filter "Category=Determinism" \
--configuration Release \
--logger "trx;LogFileName=$TRX_NAME" \
--results-directory ./TestResults/Determinism \
--verbosity minimal 2>&1; then
PASSED=$((PASSED + 1))
else
SKIPPED=$((SKIPPED + 1))
fi
echo "::endgroup::"
done
echo "## Determinism Test Summary" >> $GITHUB_STEP_SUMMARY
echo "- Passed: $PASSED" >> $GITHUB_STEP_SUMMARY
echo "- Skipped: $SKIPPED" >> $GITHUB_STEP_SUMMARY
- name: Upload Test Results
uses: actions/upload-artifact@v4
if: always()
with:
name: test-results-determinism
path: ./TestResults/Determinism
retention-days: 14
resilience:
name: Resilience Tests
runs-on: ubuntu-22.04
timeout-minutes: 45
needs: discover
if: github.event.inputs.include_resilience == 'true'
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 }}
include-prerelease: true
- name: Run Resilience Tests (all test projects)
run: |
mkdir -p ./TestResults/Resilience
FAILED=0
PASSED=0
SKIPPED=0
for proj in $(find src \( -name "*.Tests.csproj" -o -name "*UnitTests.csproj" -o -name "*SmokeTests.csproj" -o -name "*FixtureTests.csproj" -o -name "*IntegrationTests.csproj" \) -type f ! -path "*/node_modules/*" ! -name "StellaOps.TestKit.csproj" ! -name "*Testing.csproj" | sort); do
echo "::group::Testing $proj"
TRX_NAME=$(echo "$proj" | sed 's|/|_|g' | sed 's|\.csproj||')-resilience.trx
if dotnet test "$proj" \
--filter "Category=Resilience" \
--configuration Release \
--logger "trx;LogFileName=$TRX_NAME" \
--results-directory ./TestResults/Resilience \
--verbosity minimal 2>&1; then
PASSED=$((PASSED + 1))
else
SKIPPED=$((SKIPPED + 1))
fi
echo "::endgroup::"
done
echo "## Resilience Test Summary" >> $GITHUB_STEP_SUMMARY
echo "- Passed: $PASSED" >> $GITHUB_STEP_SUMMARY
echo "- Skipped: $SKIPPED" >> $GITHUB_STEP_SUMMARY
- name: Upload Test Results
uses: actions/upload-artifact@v4
if: always()
with:
name: test-results-resilience
path: ./TestResults/Resilience
retention-days: 14
observability:
name: Observability Tests
runs-on: ubuntu-22.04
timeout-minutes: 30
needs: discover
if: github.event.inputs.include_observability == 'true'
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 }}
include-prerelease: true
- name: Run Observability Tests (all test projects)
run: |
mkdir -p ./TestResults/Observability
FAILED=0
PASSED=0
SKIPPED=0
for proj in $(find src \( -name "*.Tests.csproj" -o -name "*UnitTests.csproj" -o -name "*SmokeTests.csproj" -o -name "*FixtureTests.csproj" -o -name "*IntegrationTests.csproj" \) -type f ! -path "*/node_modules/*" ! -name "StellaOps.TestKit.csproj" ! -name "*Testing.csproj" | sort); do
echo "::group::Testing $proj"
TRX_NAME=$(echo "$proj" | sed 's|/|_|g' | sed 's|\.csproj||')-observability.trx
if dotnet test "$proj" \
--filter "Category=Observability" \
--configuration Release \
--logger "trx;LogFileName=$TRX_NAME" \
--results-directory ./TestResults/Observability \
--verbosity minimal 2>&1; then
PASSED=$((PASSED + 1))
else
SKIPPED=$((SKIPPED + 1))
fi
echo "::endgroup::"
done
echo "## Observability Test Summary" >> $GITHUB_STEP_SUMMARY
echo "- Passed: $PASSED" >> $GITHUB_STEP_SUMMARY
echo "- Skipped: $SKIPPED" >> $GITHUB_STEP_SUMMARY
- name: Upload Test Results
uses: actions/upload-artifact@v4
if: always()
with:
name: test-results-observability
path: ./TestResults/Observability
name: test-results-${{ matrix.category }}
path: ./TestResults/${{ matrix.category }}
retention-days: 14
# ===========================================================================
@@ -855,7 +322,7 @@ jobs:
summary:
name: Test Summary
runs-on: ubuntu-22.04
needs: [discover, unit, architecture, contract, integration, security, golden]
needs: [discover, pr-gating-tests, integration]
if: always()
steps:
- name: Download all test results
@@ -885,18 +352,14 @@ jobs:
echo "| Category | Status |" >> $GITHUB_STEP_SUMMARY
echo "|----------|--------|" >> $GITHUB_STEP_SUMMARY
echo "| Discover | ${{ needs.discover.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Unit | ${{ needs.unit.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Architecture | ${{ needs.architecture.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Contract | ${{ needs.contract.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| PR-Gating Matrix | ${{ needs.pr-gating-tests.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Integration | ${{ needs.integration.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Security | ${{ needs.security.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Golden | ${{ needs.golden.result }} |" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Test Projects Discovered: ${{ needs.discover.outputs.test-count }}" >> $GITHUB_STEP_SUMMARY
- name: Count TRX files
run: |
TRX_COUNT=$(find ./TestResults -name "*.trx" | wc -l)
TRX_COUNT=$(find ./TestResults -name "*.trx" 2>/dev/null | wc -l || echo "0")
echo "### Total TRX Files Generated: $TRX_COUNT" >> $GITHUB_STEP_SUMMARY
- name: Upload Combined Results