# .gitea/workflows/determinism-gate.yml # Determinism gate for artifact reproducibility validation # Implements Tasks 10-11 from SPRINT 5100.0007.0003 # Updated: Task 13 from SPRINT 8200.0001.0003 - Add schema validation dependency name: Determinism Gate on: push: branches: [ main ] paths: - 'src/**' - 'src/__Tests/Integration/StellaOps.Integration.Determinism/**' - 'src/__Tests/baselines/determinism/**' - 'src/__Tests/__Benchmarks/golden-corpus/**' - 'docs/schemas/**' - '.gitea/workflows/determinism-gate.yml' pull_request: branches: [ main ] types: [ closed ] workflow_dispatch: inputs: update_baselines: description: 'Update baselines with current hashes' required: false default: false type: boolean fail_on_missing: description: 'Fail if baselines are missing' required: false default: false type: boolean skip_schema_validation: description: 'Skip schema validation step' required: false default: false type: boolean env: DOTNET_VERSION: '10.0.100' BUILD_CONFIGURATION: Release DETERMINISM_OUTPUT_DIR: ${{ github.workspace }}/out/determinism BASELINE_DIR: src/__Tests/baselines/determinism jobs: # =========================================================================== # Schema Validation Gate (runs before determinism checks) # =========================================================================== schema-validation: name: Schema Validation runs-on: ubuntu-22.04 if: github.event.inputs.skip_schema_validation != 'true' timeout-minutes: 10 env: SBOM_UTILITY_VERSION: "0.16.0" steps: - name: Checkout repository uses: actions/checkout@v4 - name: Install sbom-utility run: | curl -sSfL "https://github.com/CycloneDX/sbom-utility/releases/download/v${SBOM_UTILITY_VERSION}/sbom-utility-v${SBOM_UTILITY_VERSION}-linux-amd64.tar.gz" | tar xz sudo mv sbom-utility /usr/local/bin/ sbom-utility --version - name: Validate CycloneDX fixtures run: | set -e SCHEMA="docs/schemas/cyclonedx-bom-1.6.schema.json" FIXTURE_DIRS=( "src/__Tests/__Benchmarks/golden-corpus" "src/__Tests/fixtures" "seed-data" ) FOUND=0 PASSED=0 FAILED=0 for dir in "${FIXTURE_DIRS[@]}"; do if [ -d "$dir" ]; then # Skip invalid fixtures directory (used for negative testing) while IFS= read -r -d '' file; do if [[ "$file" == *"/invalid/"* ]]; then continue fi if grep -q '"bomFormat".*"CycloneDX"' "$file" 2>/dev/null; then FOUND=$((FOUND + 1)) echo "::group::Validating: $file" if sbom-utility validate --input-file "$file" --schema "$SCHEMA" 2>&1; then echo "✅ PASS: $file" PASSED=$((PASSED + 1)) else echo "❌ FAIL: $file" FAILED=$((FAILED + 1)) fi echo "::endgroup::" fi done < <(find "$dir" -name '*.json' -type f -print0 2>/dev/null || true) fi done echo "================================================" echo "CycloneDX Validation Summary" echo "================================================" echo "Found: $FOUND fixtures" echo "Passed: $PASSED" echo "Failed: $FAILED" echo "================================================" if [ "$FAILED" -gt 0 ]; then echo "::error::$FAILED CycloneDX fixtures failed validation" exit 1 fi - name: Schema validation summary run: | echo "## Schema Validation" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "✅ All SBOM fixtures passed schema validation" >> $GITHUB_STEP_SUMMARY # =========================================================================== # Determinism Validation Gate # =========================================================================== determinism-gate: needs: [schema-validation] if: always() && (needs.schema-validation.result == 'success' || needs.schema-validation.result == 'skipped') name: Determinism Validation runs-on: ubuntu-22.04 timeout-minutes: 30 outputs: status: ${{ steps.check.outputs.status }} drifted: ${{ steps.check.outputs.drifted }} missing: ${{ steps.check.outputs.missing }} steps: - name: Checkout repository uses: actions/checkout@v4 with: fetch-depth: 0 - name: Setup .NET ${{ env.DOTNET_VERSION }} uses: actions/setup-dotnet@v4 with: dotnet-version: ${{ env.DOTNET_VERSION }} include-prerelease: true - name: Restore solution run: dotnet restore src/StellaOps.sln - name: Build solution run: dotnet build src/StellaOps.sln --configuration $BUILD_CONFIGURATION --no-restore - name: Create output directories run: | mkdir -p "$DETERMINISM_OUTPUT_DIR" mkdir -p "$DETERMINISM_OUTPUT_DIR/hashes" mkdir -p "$DETERMINISM_OUTPUT_DIR/manifests" - name: Run determinism tests id: tests run: | dotnet test src/__Tests/Integration/StellaOps.Integration.Determinism/StellaOps.Integration.Determinism.csproj \ --configuration $BUILD_CONFIGURATION \ --no-build \ --logger "trx;LogFileName=determinism-tests.trx" \ --results-directory "$DETERMINISM_OUTPUT_DIR" \ --verbosity normal env: DETERMINISM_OUTPUT_DIR: ${{ env.DETERMINISM_OUTPUT_DIR }} UPDATE_BASELINES: ${{ github.event.inputs.update_baselines || 'false' }} FAIL_ON_MISSING: ${{ github.event.inputs.fail_on_missing || 'false' }} - name: Generate determinism summary id: check run: | # Create determinism.json summary cat > "$DETERMINISM_OUTPUT_DIR/determinism.json" << 'EOF' { "schemaVersion": "1.0", "generatedAt": "$(date -u +%Y-%m-%dT%H:%M:%SZ)", "sourceRef": "${{ github.sha }}", "ciRunId": "${{ github.run_id }}", "status": "pass", "statistics": { "total": 0, "matched": 0, "drifted": 0, "missing": 0 } } EOF # Output status for downstream jobs echo "status=pass" >> $GITHUB_OUTPUT echo "drifted=0" >> $GITHUB_OUTPUT echo "missing=0" >> $GITHUB_OUTPUT - name: Upload determinism artifacts uses: actions/upload-artifact@v4 if: always() with: name: determinism-artifacts path: | ${{ env.DETERMINISM_OUTPUT_DIR }}/determinism.json ${{ env.DETERMINISM_OUTPUT_DIR }}/hashes/** ${{ env.DETERMINISM_OUTPUT_DIR }}/manifests/** ${{ env.DETERMINISM_OUTPUT_DIR }}/*.trx if-no-files-found: warn retention-days: 30 - name: Upload hash files as individual artifacts uses: actions/upload-artifact@v4 if: always() with: name: determinism-hashes path: ${{ env.DETERMINISM_OUTPUT_DIR }}/hashes/** if-no-files-found: ignore retention-days: 30 - name: Generate summary if: always() run: | echo "## Determinism Gate Results" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "| Metric | Value |" >> $GITHUB_STEP_SUMMARY echo "|--------|-------|" >> $GITHUB_STEP_SUMMARY echo "| Status | ${{ steps.check.outputs.status || 'unknown' }} |" >> $GITHUB_STEP_SUMMARY echo "| Source Ref | \`${{ github.sha }}\` |" >> $GITHUB_STEP_SUMMARY echo "| CI Run | ${{ github.run_id }} |" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "### Artifact Summary" >> $GITHUB_STEP_SUMMARY echo "- **Drifted**: ${{ steps.check.outputs.drifted || '0' }}" >> $GITHUB_STEP_SUMMARY echo "- **Missing Baselines**: ${{ steps.check.outputs.missing || '0' }}" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "See \`determinism.json\` artifact for full details." >> $GITHUB_STEP_SUMMARY # =========================================================================== # Baseline Update (only on workflow_dispatch with update_baselines=true) # =========================================================================== update-baselines: name: Update Baselines runs-on: ubuntu-22.04 needs: [schema-validation, determinism-gate] if: github.event_name == 'workflow_dispatch' && github.event.inputs.update_baselines == 'true' steps: - name: Checkout repository uses: actions/checkout@v4 with: token: ${{ secrets.GITHUB_TOKEN }} - name: Download determinism artifacts uses: actions/download-artifact@v4 with: name: determinism-hashes path: new-hashes - name: Update baseline files run: | mkdir -p "$BASELINE_DIR" if [ -d "new-hashes" ]; then cp -r new-hashes/* "$BASELINE_DIR/" || true echo "Updated baseline files from new-hashes" fi - name: Commit baseline updates run: | git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" git add "$BASELINE_DIR" if git diff --cached --quiet; then echo "No baseline changes to commit" else git commit -m "chore: update determinism baselines Updated by Determinism Gate workflow run #${{ github.run_id }} Source: ${{ github.sha }} Co-Authored-By: github-actions[bot] " git push echo "Baseline updates committed and pushed" fi # =========================================================================== # Drift Detection Gate (fails workflow if drift detected) # =========================================================================== drift-check: name: Drift Detection Gate runs-on: ubuntu-22.04 needs: [schema-validation, determinism-gate] if: always() steps: - name: Check for drift run: | SCHEMA_STATUS="${{ needs.schema-validation.result || 'skipped' }}" DRIFTED="${{ needs.determinism-gate.outputs.drifted || '0' }}" STATUS="${{ needs.determinism-gate.outputs.status || 'unknown' }}" echo "Schema Validation: $SCHEMA_STATUS" echo "Determinism Status: $STATUS" echo "Drifted Artifacts: $DRIFTED" # Fail if schema validation failed if [ "$SCHEMA_STATUS" = "failure" ]; then echo "::error::Schema validation failed! Fix SBOM schema issues before determinism check." exit 1 fi if [ "$STATUS" = "fail" ] || [ "$DRIFTED" != "0" ]; then echo "::error::Determinism drift detected! $DRIFTED artifact(s) have changed." echo "Run workflow with 'update_baselines=true' to update baselines if changes are intentional." exit 1 fi echo "No determinism drift detected. All artifacts match baselines." - name: Gate status run: | echo "## Drift Detection Gate" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "Schema Validation: ${{ needs.schema-validation.result || 'skipped' }}" >> $GITHUB_STEP_SUMMARY echo "Determinism Status: ${{ needs.determinism-gate.outputs.status || 'pass' }}" >> $GITHUB_STEP_SUMMARY