Merge remote-tracking branch 'origin/main' into dependabot/npm_and_ya… #735
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Comprehensive Test Suite | ||
| on: | ||
| push: | ||
| branches: [main, develop] | ||
| pull_request: | ||
| branches: [main, develop] | ||
| env: | ||
| CARGO_TERM_COLOR: always | ||
| RUST_BACKTRACE: 1 | ||
| # Enforced by cargo llvm-cov --fail-under-lines (line %, package-scoped runs). | ||
| COVERAGE_FAIL_LINES_PREDICT_IQ: "80" | ||
| COVERAGE_FAIL_LINES_PREDICTIQ_API: "17" | ||
| # Soroban contract WASM size limit (64 KB internal budget target) | ||
| # Soroban's actual limit is larger, but we enforce this stricter threshold | ||
| # to ensure contract remains performant and deployable across all networks. | ||
| WASM_SIZE_LIMIT_BYTES: "65536" | ||
| jobs: | ||
| prometheus-rules-test: | ||
| name: Prometheus SLO Rules Testing | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| - name: Install Prometheus tools | ||
| run: | | ||
| wget https://github.com/prometheus/prometheus/releases/download/v2.45.0/prometheus-2.45.0.linux-amd64.tar.gz | ||
| tar xzf prometheus-2.45.0.linux-amd64.tar.gz | ||
| sudo mv prometheus-2.45.0.linux-amd64/promtool /usr/local/bin/ | ||
| - name: Check Prometheus rules syntax | ||
| run: promtool check rules performance/config/prometheus-slo-rules.yml | ||
| - name: Check alert rules syntax | ||
| run: promtool check rules performance/config/alerts.yaml | ||
| - name: Run Prometheus rules unit tests | ||
| run: promtool test rules performance/config/prometheus-slo-rules.test.yml | ||
| unit-tests: | ||
| name: Unit Tests | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| - name: Install Rust | ||
| uses: dtolnay/rust-toolchain@stable | ||
| - name: Cache cargo registry | ||
| uses: actions/cache@v5 | ||
| with: | ||
| path: ~/.cargo/registry | ||
| key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }} | ||
| - name: Cache cargo index | ||
| uses: actions/cache@v5 | ||
| with: | ||
| path: ~/.cargo/git | ||
| key: ${{ runner.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }} | ||
| - name: Cache cargo build | ||
| uses: actions/cache@v5 | ||
| with: | ||
| path: target | ||
| key: ${{ runner.os }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }} | ||
| - name: Install Soroban CLI | ||
| run: | | ||
| cargo install --locked soroban-cli --features opt | ||
| - name: Run unit tests | ||
| run: cargo test --lib --workspace | ||
| working-directory: contracts/predict-iq | ||
| - name: Run module tests | ||
| run: cargo test --lib --features testutils | ||
| working-directory: contracts/predict-iq | ||
| integration-tests: | ||
| name: Integration Tests | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| - name: Install Rust | ||
| uses: dtolnay/rust-toolchain@stable | ||
| - name: Cache dependencies | ||
| uses: actions/cache@v5 | ||
| with: | ||
| path: | | ||
| ~/.cargo/registry | ||
| ~/.cargo/git | ||
| target | ||
| key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} | ||
| - name: Install Soroban CLI | ||
| run: cargo install --locked soroban-cli --features opt | ||
| - name: Run integration tests | ||
| run: cargo test --test '*' --workspace | ||
| working-directory: contracts/predict-iq | ||
| api-rate-limit-tests: | ||
| name: API Rate Limiting Integration Tests | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| - name: Install Rust | ||
| uses: dtolnay/rust-toolchain@stable | ||
| - name: Cache dependencies | ||
| uses: actions/cache@v5 | ||
| with: | ||
| path: | | ||
| ~/.cargo/registry | ||
| ~/.cargo/git | ||
| target | ||
| key: ${{ runner.os }}-cargo-api-${{ hashFiles('services/api/Cargo.lock') }} | ||
| - name: Run rate limiting integration tests | ||
| run: cargo test --test rate_limiting_tests | ||
| working-directory: services/api | ||
| gas-benchmarks: | ||
| name: Gas Benchmarks | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| - name: Install Rust | ||
| uses: dtolnay/rust-toolchain@stable | ||
| - name: Cache dependencies | ||
| uses: actions/cache@v5 | ||
| with: | ||
| path: | | ||
| ~/.cargo/registry | ||
| ~/.cargo/git | ||
| target | ||
| key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} | ||
| - name: Install Soroban CLI | ||
| run: cargo install --locked soroban-cli --features opt | ||
| - name: Run gas benchmarks | ||
| run: cargo test --benches --features testutils 2>&1 | tee benchmark-output.txt | ||
| working-directory: contracts/predict-iq | ||
| - name: Setup Node.js | ||
| uses: actions/setup-node@v6 | ||
| with: | ||
| node-version: "18" | ||
| - name: Compare gas benchmarks against baseline | ||
| working-directory: contracts/predict-iq | ||
| run: | | ||
| node scripts/compare-gas-benchmarks.js --branch main --threshold 10 | ||
| - name: Upload benchmark report | ||
| if: always() | ||
| uses: actions/upload-artifact@v7 | ||
| with: | ||
| name: gas-benchmark-report | ||
| path: contracts/predict-iq/target/gas-benchmark-report.md | ||
| retention-days: 30 | ||
| - name: Comment PR with benchmark results | ||
| if: github.event_name == 'pull_request' && always() | ||
| uses: actions/github-script@v9 | ||
| with: | ||
| script: | | ||
| const fs = require('fs'); | ||
| const path = 'contracts/predict-iq/target/gas-benchmark-report.md'; | ||
| if (fs.existsSync(path)) { | ||
| const report = fs.readFileSync(path, 'utf8'); | ||
| github.rest.issues.createComment({ | ||
| issue_number: context.issue.number, | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| body: report | ||
| }); | ||
| } | ||
| - name: Fail if gas regression detected | ||
| if: failure() && github.event_name == 'pull_request' | ||
| run: | | ||
| echo "❌ Gas regression detected. Check the PR comment for details." | ||
| exit 1 | ||
| save-gas-baseline: | ||
| name: Save Gas Benchmark Baseline | ||
| runs-on: ubuntu-latest | ||
| needs: [gas-benchmarks] | ||
| if: github.event_name == 'push' && github.ref == 'refs/heads/main' | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| - name: Install Rust | ||
| uses: dtolnay/rust-toolchain@stable | ||
| - name: Cache dependencies | ||
| uses: actions/cache@v5 | ||
| with: | ||
| path: | | ||
| ~/.cargo/registry | ||
| ~/.cargo/git | ||
| target | ||
| key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} | ||
| - name: Install Soroban CLI | ||
| run: cargo install --locked soroban-cli --features opt | ||
| - name: Run gas benchmarks | ||
| run: cargo test --benches --features testutils | ||
| working-directory: contracts/predict-iq | ||
| - name: Setup Node.js | ||
| uses: actions/setup-node@v6 | ||
| with: | ||
| node-version: "18" | ||
| - name: Save baseline for main branch | ||
| working-directory: contracts/predict-iq | ||
| run: | | ||
| node scripts/compare-gas-benchmarks.js --save-baseline --branch main | ||
| - name: Create Pull Request with baseline updates | ||
| uses: peter-evans/create-pull-request@v6 | ||
| with: | ||
| commit-message: "chore: update gas benchmark baseline for main branch" | ||
| title: "chore: update gas benchmark baseline" | ||
| body: | | ||
| ## Automated Gas Benchmark Baseline Update | ||
| This PR updates the gas benchmark baseline after changes to main branch. | ||
| **Changes:** | ||
| - Updated `.gas-benchmarks/` files with latest benchmark results | ||
| **Note:** This is an automated PR. Please review the baseline changes before merging. | ||
| branch: chore/gas-baseline-${{ github.run_id }} | ||
| delete-branch: true | ||
| labels: | | ||
| ci/cd | ||
| automated | ||
| backend-coverage: | ||
| name: Backend coverage (contracts + API) | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| - name: Install Rust | ||
| uses: dtolnay/rust-toolchain@stable | ||
| with: | ||
| components: llvm-tools-preview | ||
| - name: Install cargo-llvm-cov | ||
| run: cargo install cargo-llvm-cov --locked | ||
| - name: Install Soroban CLI | ||
| run: cargo install --locked soroban-cli --features opt | ||
| - name: Prepare coverage output directory | ||
| run: mkdir -p coverage-reports | ||
| - name: Coverage gate — predict-iq (Soroban contract) | ||
| env: | ||
| CARGO_LLVM_COV_TARGET_DIR: ${{ github.workspace }}/llvm-cov-target/predict-iq | ||
| run: > | ||
| cargo llvm-cov -p predict-iq --all-features | ||
| --fail-under-lines ${COVERAGE_FAIL_LINES_PREDICT_IQ} | ||
| --lcov --output-path coverage-reports/predict-iq.lcov | ||
| --html --output-dir coverage-reports/predict-iq-html | ||
| - name: Coverage gate — predictiq-api (Rust API service) | ||
| env: | ||
| CARGO_LLVM_COV_TARGET_DIR: ${{ github.workspace }}/llvm-cov-target/predictiq-api | ||
| working-directory: services/api | ||
| run: > | ||
| cargo llvm-cov | ||
| --fail-under-lines ${COVERAGE_FAIL_LINES_PREDICTIQ_API} | ||
| --lcov --output-path ../../coverage-reports/predictiq-api.lcov | ||
| --html --output-dir ../../coverage-reports/predictiq-api-html | ||
| - name: Upload HTML + LCOV artifacts | ||
| uses: actions/upload-artifact@v7 | ||
| with: | ||
| name: backend-coverage | ||
| path: | | ||
| coverage-reports/*.lcov | ||
| coverage-reports/predict-iq-html | ||
| coverage-reports/predictiq-api-html | ||
| if-no-files-found: error | ||
| - name: Upload predict-iq LCOV to Codecov | ||
| uses: codecov/codecov-action@v4 | ||
| with: | ||
| files: coverage-reports/predict-iq.lcov | ||
| flags: predict-iq | ||
| fail_ci_if_error: true | ||
| verbose: true | ||
| - name: Upload predictiq-api LCOV to Codecov | ||
| uses: codecov/codecov-action@v4 | ||
| with: | ||
| files: coverage-reports/predictiq-api.lcov | ||
| flags: predictiq-api | ||
| fail_ci_if_error: true | ||
| verbose: true | ||
| security-audit: | ||
| name: Security Audit | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| - name: Install Rust | ||
| uses: dtolnay/rust-toolchain@stable | ||
| - name: Install cargo-audit | ||
| run: cargo install cargo-audit | ||
| - name: Run security audit — contracts/predict-iq | ||
| run: cargo audit | ||
| working-directory: contracts/predict-iq | ||
| - name: Run security audit — services/api | ||
| run: cargo audit | ||
| working-directory: services/api | ||
| sast-scanning: | ||
| name: SAST Security Scanning | ||
| runs-on: ubuntu-latest | ||
| permissions: | ||
| security-events: write | ||
| contents: read | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| - name: Run Semgrep SAST | ||
| uses: returntocorp/semgrep-action@v1 | ||
| with: | ||
| config: >- | ||
| p/security-audit | ||
| p/rust | ||
| p/nodejs | ||
| p/typescript | ||
| p/javascript | ||
| generateSarif: true | ||
| - name: Upload Semgrep SARIF | ||
| uses: github/codeql-action/upload-sarif@v4 | ||
| if: always() | ||
| with: | ||
| sarif_file: semgrep.sarif | ||
| - name: Fail on critical findings | ||
| run: | | ||
| if [ -f semgrep.sarif ]; then | ||
| critical_count=$(jq '[.runs[].results[] | select(.level == "error")] | length' semgrep.sarif) | ||
| if [ "$critical_count" -gt 0 ]; then | ||
| echo "Found $critical_count critical security findings" | ||
| exit 1 | ||
| fi | ||
| fi | ||
| secrets-scanning: | ||
| name: Secrets Scanning | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| with: | ||
| fetch-depth: 0 | ||
| - name: Run Gitleaks | ||
| uses: gitleaks/gitleaks-action@v2 | ||
| env: | ||
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
| GITLEAKS_LICENSE: ${{ secrets.GITLEAKS_LICENSE }} | ||
| - name: Run TruffleHog | ||
| uses: trufflesecurity/trufflehog@9f0b97f1600cd5f51e5ecb8380087807acb790f9 # main | ||
| with: | ||
| path: ./ | ||
| base: ${{ github.event.repository.default_branch }} | ||
| head: HEAD | ||
| extra_args: --only-verified | ||
| dependency-scanning: | ||
| name: Dependency Vulnerability Scanning | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| - name: Run Trivy vulnerability scanner | ||
| uses: aquasecurity/trivy-action@314ff8b43182423b84c50b1670b0e10f858f2d98 # master | ||
| with: | ||
| scan-type: "fs" | ||
| scan-ref: "." | ||
| format: "sarif" | ||
| output: "trivy-results.sarif" | ||
| severity: "CRITICAL,HIGH" | ||
| exit-code: "1" | ||
| - name: Upload Trivy SARIF | ||
| uses: github/codeql-action/upload-sarif@v4 | ||
| if: always() | ||
| with: | ||
| sarif_file: trivy-results.sarif | ||
| - name: Scan Rust dependencies | ||
| run: | | ||
| cargo install cargo-audit | ||
| cargo audit --deny warnings | ||
| working-directory: contracts/predict-iq | ||
| - name: Scan Node.js dependencies | ||
| # Fails CI on high/critical npm vulnerabilities. | ||
| # Exception process: for known false positives, add an entry to | ||
| # frontend/.nsprc (npm audit ignore) with a justification comment | ||
| # and link to the tracking issue, then get security team approval | ||
| # before merging. | ||
| run: | | ||
| npm audit --audit-level=high | ||
| working-directory: frontend | ||
| container-scanning: | ||
| name: Container Image Scanning | ||
| runs-on: ubuntu-latest | ||
| if: github.event_name == 'pull_request' | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| - name: Build API container image | ||
| run: docker build -t predictiq-api:${{ github.sha }} -f services/api/Dockerfile . | ||
| continue-on-error: true | ||
| - name: Scan API container with Trivy | ||
| uses: aquasecurity/trivy-action@314ff8b43182423b84c50b1670b0e10f858f2d98 # master | ||
| if: success() | ||
| with: | ||
| image-ref: "predictiq-api:${{ github.sha }}" | ||
| format: "sarif" | ||
| output: "trivy-api.sarif" | ||
| severity: "CRITICAL,HIGH" | ||
| exit-code: "1" | ||
| - name: Upload API container scan SARIF | ||
| uses: github/codeql-action/upload-sarif@v4 | ||
| if: always() | ||
| with: | ||
| sarif_file: trivy-api.sarif | ||
| category: trivy-api | ||
| - name: Build TTS container image | ||
| run: docker build -t predictiq-tts:${{ github.sha }} -f services/tts/Dockerfile services/tts/ | ||
| continue-on-error: true | ||
| - name: Scan TTS container with Trivy | ||
| uses: aquasecurity/trivy-action@314ff8b43182423b84c50b1670b0e10f858f2d98 # master | ||
| if: success() | ||
| with: | ||
| image-ref: "predictiq-tts:${{ github.sha }}" | ||
| format: "sarif" | ||
| output: "trivy-tts.sarif" | ||
| severity: "CRITICAL,HIGH" | ||
| exit-code: "1" | ||
| - name: Upload TTS container scan SARIF | ||
| uses: github/codeql-action/upload-sarif@v4 | ||
| if: always() | ||
| with: | ||
| sarif_file: trivy-tts.sarif | ||
| category: trivy-tts | ||
| codeql-analysis: | ||
| name: CodeQL Analysis | ||
| runs-on: ubuntu-latest | ||
| permissions: | ||
| security-events: write | ||
| contents: read | ||
| strategy: | ||
| matrix: | ||
| language: ["javascript", "typescript", "rust"] | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| - name: Install Rust (for Rust analysis) | ||
| if: matrix.language == 'rust' | ||
| uses: actions-rs/toolchain@v1 | ||
| with: | ||
| profile: minimal | ||
| toolchain: stable | ||
| override: true | ||
| - name: Initialize CodeQL | ||
| uses: github/codeql-action/init@v4 | ||
| with: | ||
| languages: ${{ matrix.language }} | ||
| queries: security-extended | ||
| - name: Autobuild | ||
| uses: github/codeql-action/autobuild@v4 | ||
| - name: Perform CodeQL Analysis | ||
| uses: github/codeql-action/analyze@v4 | ||
| with: | ||
| category: "/language:${{ matrix.language }}" | ||
| clippy: | ||
| name: Clippy Lints | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| - name: Install Rust | ||
| uses: dtolnay/rust-toolchain@stable | ||
| components: clippy | ||
| - name: Run Clippy | ||
| run: cargo clippy --all-targets --all-features -- -D warnings | ||
| working-directory: contracts/predict-iq | ||
| oracle-quality-gate: | ||
| name: Oracle Module — Clippy + Warnings-as-Errors | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| - name: Install Rust | ||
| uses: dtolnay/rust-toolchain@stable | ||
| components: clippy | ||
| - name: Clippy (deny warnings) — oracle module | ||
| env: | ||
| RUSTFLAGS: -D warnings | ||
| run: > | ||
| cargo clippy | ||
| --package predict-iq | ||
| --all-features | ||
| -- -D warnings | ||
| working-directory: contracts/predict-iq | ||
| - name: Build (deny warnings) — contract crate | ||
| env: | ||
| RUSTFLAGS: -D warnings | ||
| run: cargo build --package predict-iq --all-features | ||
| working-directory: contracts/predict-iq | ||
| - name: Run oracle tests | ||
| run: cargo test --package predict-iq --lib oracles | ||
| working-directory: contracts/predict-iq | ||
| format: | ||
| name: Code Formatting | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| - name: Install Rust | ||
| uses: dtolnay/rust-toolchain@stable | ||
| components: rustfmt | ||
| - name: Check formatting | ||
| run: cargo fmt --all -- --check | ||
| working-directory: contracts/predict-iq | ||
| build-optimized: | ||
| name: Build Optimized Contract | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| - name: Install Rust | ||
| uses: dtolnay/rust-toolchain@stable | ||
| target: wasm32-unknown-unknown | ||
| - name: Install Soroban CLI | ||
| run: cargo install --locked soroban-cli --features opt | ||
| - name: Build optimized WASM | ||
| run: cargo build --target wasm32-unknown-unknown --release | ||
| working-directory: contracts/predict-iq | ||
| - name: Optimize WASM | ||
| run: | | ||
| soroban contract optimize \ | ||
| --wasm target/wasm32-unknown-unknown/release/predict_iq.wasm \ | ||
| --wasm-out target/wasm32-unknown-unknown/release/predict_iq_optimized.wasm | ||
| working-directory: contracts/predict-iq | ||
| - name: Check WASM size | ||
| run: | | ||
| size=$(wc -c < target/wasm32-unknown-unknown/release/predict_iq_optimized.wasm) | ||
| echo "Optimized WASM size: $size bytes" | ||
| if [ $size -gt ${{ env.WASM_SIZE_LIMIT_BYTES }} ]; then | ||
| echo "WASM size $size exceeds limit of ${{ env.WASM_SIZE_LIMIT_BYTES }} bytes" | ||
| exit 1 | ||
| fi | ||
| working-directory: contracts/predict-iq | ||
| api-cache-tests: | ||
| name: API Cache Tests (Redis) | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| - name: Install Rust | ||
| uses: dtolnay/rust-toolchain@stable | ||
| - name: Cache cargo dependencies | ||
| uses: actions/cache@v5 | ||
| with: | ||
| path: | | ||
| ~/.cargo/registry | ||
| ~/.cargo/git | ||
| target | ||
| key: ${{ runner.os }}-cargo-api-${{ hashFiles('services/api/Cargo.lock') }} | ||
| - name: Run cache tests | ||
| run: cargo test --lib cache | ||
| working-directory: services/api | ||
| e2e-market-creation: | ||
| name: E2E — Market Creation Flow | ||
| runs-on: ubuntu-latest | ||
| defaults: | ||
| run: | ||
| working-directory: frontend | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| - name: Setup Node.js | ||
| uses: actions/setup-node@v6 | ||
| with: | ||
| node-version: "18" | ||
| cache: "npm" | ||
| cache-dependency-path: frontend/package-lock.json | ||
| - name: Install dependencies | ||
| run: npm ci | ||
| - name: Install Playwright browsers | ||
| run: npx playwright install --with-deps chromium | ||
| - name: Run market-creation E2E tests | ||
| run: npx playwright test e2e/market-creation.spec.ts --project=chromium | ||
| env: | ||
| CI: "true" | ||
| - name: Run flaky test detection | ||
| run: npx playwright test e2e/market-creation.spec.ts --project=chromium | ||
| env: | ||
| CI: "true" | ||
| FLAKY_DETECTION: "true" | ||
| - name: Upload Playwright report | ||
| if: always() | ||
| uses: actions/upload-artifact@v7 | ||
| with: | ||
| name: playwright-market-creation-report | ||
| path: frontend/playwright-report/ | ||
| retention-days: 14 | ||
| # ── #948: mocked E2E — no live backend required ─────────────────────────────── | ||
| e2e-mocked: | ||
| name: E2E — Mocked (no backend) | ||
| runs-on: ubuntu-latest | ||
| defaults: | ||
| run: | ||
| working-directory: frontend | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| - name: Setup Node.js | ||
| uses: actions/setup-node@v6 | ||
| with: | ||
| node-version: "18" | ||
| cache: "npm" | ||
| cache-dependency-path: frontend/package-lock.json | ||
| - name: Install dependencies | ||
| run: npm ci | ||
| - name: Install Playwright browsers | ||
| run: npx playwright install --with-deps chromium | ||
| - name: Run mocked E2E suite | ||
| run: | | ||
| npx playwright test \ | ||
| e2e/user-journeys.spec.ts \ | ||
| e2e/accessibility.spec.ts \ | ||
| e2e/interactions.spec.ts \ | ||
| e2e/mobile.spec.ts \ | ||
| e2e/performance.spec.ts \ | ||
| --project=chromium | ||
| env: | ||
| CI: "true" | ||
| - name: Upload Playwright report | ||
| if: always() | ||
| uses: actions/upload-artifact@v7 | ||
| with: | ||
| name: playwright-mocked-report | ||
| path: frontend/playwright-report/ | ||
| retention-days: 14 | ||
| # ── #949: visual regression tests inside pinned Docker image ───────────────── | ||
| e2e-visual-regression: | ||
| name: E2E — Visual Regression (Docker) | ||
| runs-on: ubuntu-latest | ||
| defaults: | ||
| run: | ||
| working-directory: frontend | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| - name: Build visual-regression Docker image | ||
| run: | | ||
| docker build \ | ||
| -f e2e/docker/Dockerfile.visual-regression \ | ||
| -t predictiq-visual-regression \ | ||
| . | ||
| - name: Run visual regression tests | ||
| run: | | ||
| docker run --rm \ | ||
| -v "${{ github.workspace }}/frontend/e2e:/app/e2e" \ | ||
| --network host \ | ||
| -e BASE_URL=http://localhost:3000 \ | ||
| -e CI=true \ | ||
| predictiq-visual-regression | ||
| - name: Upload Playwright report | ||
| if: always() | ||
| uses: actions/upload-artifact@v7 | ||
| with: | ||
| name: playwright-visual-regression-report | ||
| path: frontend/playwright-report/ | ||
| retention-days: 14 | ||
| # ── #950: bundle size budget ────────────────────────────────────────────────── | ||
| bundle-size: | ||
| name: Bundle Size Budget | ||
| runs-on: ubuntu-latest | ||
| defaults: | ||
| run: | ||
| working-directory: frontend | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| - name: Setup Node.js | ||
| uses: actions/setup-node@v6 | ||
| with: | ||
| node-version: "18" | ||
| cache: "npm" | ||
| cache-dependency-path: frontend/package-lock.json | ||
| - name: Install dependencies | ||
| run: npm ci | ||
| - name: Build with bundle analyzer | ||
| run: npm run build | ||
| env: | ||
| NEXT_PUBLIC_API_URL: "https://placeholder.example.com" | ||
| - name: Check bundle size budgets | ||
| run: npx bundlewatch --config bundlewatch.config.json | ||
| env: | ||
| BUNDLEWATCH_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
| # ── #740: validate every migration has a rollback script ──────────────────── | ||
| validate-migration-rollbacks: | ||
| name: Validate Migration Rollback Scripts | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| - name: Check every migration has a rollback script | ||
| run: | | ||
| MISSING=0 | ||
| MIGRATIONS_DIR="services/api/database/migrations" | ||
| ROLLBACKS_DIR="services/api/database/migrations/rollbacks" | ||
| for f in "$MIGRATIONS_DIR"/*.sql; do | ||
| base=$(basename "$f" .sql) | ||
| down="${ROLLBACKS_DIR}/${base}_down.sql" | ||
| if [ ! -f "$down" ]; then | ||
| echo "❌ Missing rollback: $down (for $f)" | ||
| MISSING=1 | ||
| else | ||
| echo "✅ $down" | ||
| fi | ||
| done | ||
| if [ "$MISSING" -eq 1 ]; then | ||
| echo "" | ||
| echo "One or more migrations are missing rollback scripts." | ||
| echo "Add the corresponding *_down.sql file in $ROLLBACKS_DIR." | ||
| exit 1 | ||
| fi | ||
| echo "" | ||
| echo "All migration files have corresponding rollback scripts." | ||
| - name: Validate rollback SQL syntax (dry-run) | ||
| run: | | ||
| for f in services/api/database/migrations/rollbacks/*_down.sql; do | ||
| # psql --dry-run is not universally available; use a syntax check via | ||
| # pg_format or simply assert the file is non-empty and contains SQL keywords. | ||
| if [ ! -s "$f" ]; then | ||
| echo "❌ Empty rollback script: $f" | ||
| exit 1 | ||
| fi | ||
| if ! grep -qiE "^(DROP|ALTER|DELETE|TRUNCATE)" "$f"; then | ||
| echo "❌ Rollback script appears to contain no DDL/DML: $f" | ||
| exit 1 | ||
| fi | ||
| echo "✅ $f" | ||
| done | ||
| # ── #906: verify rollback scripts are idempotent (run each twice) ──────────── | ||
| rollback-idempotency: | ||
| name: Rollback Idempotency Check | ||
| runs-on: ubuntu-latest | ||
| services: | ||
| postgres: | ||
| image: postgres:16 | ||
| env: | ||
| POSTGRES_USER: postgres | ||
| POSTGRES_PASSWORD: postgres | ||
| POSTGRES_DB: predictiq_test | ||
| ports: | ||
| - 5432:5432 | ||
| options: >- | ||
| --health-cmd="pg_isready -U postgres" | ||
| --health-interval=10s | ||
| --health-timeout=5s | ||
| --health-retries=5 | ||
| env: | ||
| DATABASE_URL: postgres://postgres:postgres@localhost:5432/predictiq_test | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| - name: Apply all forward migrations | ||
| run: | | ||
| for f in $(ls services/api/database/migrations/*.sql | sort); do | ||
| psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -f "$f" | ||
| done | ||
| - name: Run every rollback script twice and verify second run exits 0 | ||
| run: | | ||
| FAILED=0 | ||
| for f in $(ls services/api/database/migrations/rollbacks/*_down.sql | sort -r); do | ||
| echo "--- First run: $f ---" | ||
| psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -f "$f" || { echo "❌ First run failed: $f"; FAILED=1; continue; } | ||
| echo "--- Second run (idempotency check): $f ---" | ||
| psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -f "$f" | ||
| RC=$? | ||
| if [ $RC -ne 0 ]; then | ||
| echo "❌ Second run of $f exited with code $RC (not idempotent)" | ||
| FAILED=1 | ||
| else | ||
| echo "✅ $f is idempotent" | ||
| fi | ||
| done | ||
| if [ "$FAILED" -eq 1 ]; then | ||
| echo "One or more rollback scripts are not idempotent." | ||
| exit 1 | ||
| fi | ||
| echo "All rollback scripts passed the idempotency check." | ||
| # ── #741: API Criterion benchmarks ─────────────────────────────────────────── | ||
| api-criterion-benchmarks: | ||
| name: API Criterion Benchmarks | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| - name: Install Rust | ||
| uses: dtolnay/rust-toolchain@stable | ||
| - name: Cache cargo dependencies | ||
| uses: actions/cache@v4 | ||
| with: | ||
| path: | | ||
| ~/.cargo/registry | ||
| ~/.cargo/git | ||
| target | ||
| key: ${{ runner.os }}-cargo-api-bench-${{ hashFiles('services/api/Cargo.lock') }} | ||
| - name: Run API benchmarks | ||
| run: cargo bench 2>&1 | tee ../../bench-output.txt | ||
| working-directory: services/api | ||
| - name: Setup Node.js | ||
| uses: actions/setup-node@v3 | ||
| with: | ||
| node-version: "18" | ||
| - name: Parse benchmark output | ||
| run: | | ||
| node services/api/scripts/parse-bench-output.js \ | ||
| bench-output.txt bench-results.json | ||
| - name: Fetch baseline from benchmarks branch | ||
| if: github.event_name == 'pull_request' | ||
| run: | | ||
| git fetch origin benchmarks:refs/remotes/origin/benchmarks 2>/dev/null || true | ||
| if git show origin/benchmarks:api-benchmark-baseline.json > baseline-from-branch.json 2>/dev/null; then | ||
| echo "BASELINE_FILE=baseline-from-branch.json" >> "$GITHUB_ENV" | ||
| else | ||
| echo "BASELINE_FILE=services/api/benches/.benchmarks/baseline.json" >> "$GITHUB_ENV" | ||
| echo "No dedicated branch baseline found, falling back to repo baseline." | ||
| fi | ||
| - name: Compare against baseline | ||
| if: github.event_name == 'pull_request' | ||
| run: | | ||
| node services/api/scripts/compare-api-benchmarks.js \ | ||
| bench-results.json \ | ||
| "$BASELINE_FILE" \ | ||
| --threshold 20 | ||
| - name: Upload benchmark results | ||
| if: always() | ||
| uses: actions/upload-artifact@v4 | ||
| with: | ||
| name: api-benchmark-results | ||
| path: | | ||
| bench-output.txt | ||
| bench-results.json | ||
| retention-days: 30 | ||
| save-api-bench-baseline: | ||
| name: Save API Benchmark Baseline | ||
| runs-on: ubuntu-latest | ||
| needs: [api-criterion-benchmarks] | ||
| if: github.event_name == 'push' && github.ref == 'refs/heads/main' | ||
| permissions: | ||
| contents: write | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| with: | ||
| fetch-depth: 0 | ||
| - name: Install Rust | ||
| uses: dtolnay/rust-toolchain@stable | ||
| - name: Cache cargo dependencies | ||
| uses: actions/cache@v4 | ||
| with: | ||
| path: | | ||
| ~/.cargo/registry | ||
| ~/.cargo/git | ||
| target | ||
| key: ${{ runner.os }}-cargo-api-bench-${{ hashFiles('services/api/Cargo.lock') }} | ||
| - name: Run API benchmarks | ||
| run: cargo bench 2>&1 | tee ../../bench-output.txt | ||
| working-directory: services/api | ||
| - name: Setup Node.js | ||
| uses: actions/setup-node@v3 | ||
| with: | ||
| node-version: "18" | ||
| - name: Parse benchmark results | ||
| run: | | ||
| node services/api/scripts/parse-bench-output.js \ | ||
| bench-output.txt \ | ||
| api-benchmark-baseline.json | ||
| - name: Push baseline to benchmarks branch | ||
| run: | | ||
| git config user.name "github-actions[bot]" | ||
| git config user.email "github-actions[bot]@users.noreply.github.com" | ||
| # Switch to (or create) the dedicated benchmarks branch. | ||
| git fetch origin benchmarks 2>/dev/null || true | ||
| if git show-ref --verify --quiet refs/remotes/origin/benchmarks; then | ||
| git checkout -B benchmarks origin/benchmarks | ||
| else | ||
| git checkout --orphan benchmarks | ||
| git rm -rf . --quiet | ||
| fi | ||
| cp api-benchmark-baseline.json . | ||
| git add api-benchmark-baseline.json | ||
| git commit --allow-empty -m "chore: update API benchmark baseline (run ${{ github.run_id }})" | ||
| git push origin benchmarks | ||
| # ── #743: frontend unit-test coverage gate ─────────────────────────────────── | ||
| frontend-unit-coverage: | ||
| name: Frontend Unit Test Coverage | ||
| runs-on: ubuntu-latest | ||
| defaults: | ||
| run: | ||
| working-directory: frontend | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| - name: Setup Node.js | ||
| uses: actions/setup-node@v3 | ||
| with: | ||
| node-version: "18" | ||
| cache: "npm" | ||
| cache-dependency-path: frontend/package-lock.json | ||
| - name: Install dependencies | ||
| run: npm ci | ||
| - name: Run Jest with coverage | ||
| run: npm run test:ci | ||
| env: | ||
| CI: "true" | ||
| - name: Upload coverage report | ||
| if: always() | ||
| uses: actions/upload-artifact@v4 | ||
| with: | ||
| name: frontend-coverage-report | ||
| path: frontend/coverage/ | ||
| retention-days: 14 | ||
| # ── #742: schema drift detection ───────────────────────────────────────────── | ||
| schema-drift-check: | ||
| name: Schema Drift Detection | ||
| runs-on: ubuntu-latest | ||
| services: | ||
| postgres: | ||
| image: postgres:16 | ||
| env: | ||
| POSTGRES_USER: postgres | ||
| POSTGRES_PASSWORD: postgres | ||
| POSTGRES_DB: predictiq_schema_check | ||
| ports: | ||
| - 5432:5432 | ||
| options: >- | ||
| --health-cmd pg_isready | ||
| --health-interval 10s | ||
| --health-timeout 5s | ||
| --health-retries 5 | ||
| env: | ||
| PGUSER: postgres | ||
| PGPASSWORD: postgres | ||
| PGHOST: localhost | ||
| PGDATABASE: predictiq_schema_check | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| - name: Apply all migrations | ||
| run: | | ||
| for f in $(ls services/api/database/migrations/*.sql | sort); do | ||
| echo "▶ $f" | ||
| psql -f "$f" || { echo "❌ Migration failed: $f"; exit 1; } | ||
| done | ||
| - name: Dump live schema | ||
| run: | | ||
| pg_dump \ | ||
| --schema-only \ | ||
| --no-owner \ | ||
| --no-privileges \ | ||
| --no-comments \ | ||
| --schema=public \ | ||
| > /tmp/live-schema.sql | ||
| - name: Normalise both dumps for comparison | ||
| run: | | ||
| # Strip pg_dump header lines (SET …, SELECT …) that are not structural. | ||
| normalize() { | ||
| grep -vE '^(SET |SELECT |--|$)' "$1" \ | ||
| | sed 's/ *$//; s/\t/ /g' \ | ||
| | sort | ||
| } | ||
| normalize /tmp/live-schema.sql > /tmp/live-normalized.sql | ||
| normalize services/api/database/schema.sql > /tmp/snap-normalized.sql | ||
| - name: Compare against committed snapshot | ||
| run: | | ||
| if ! diff -u /tmp/snap-normalized.sql /tmp/live-normalized.sql; then | ||
| echo "" | ||
| echo "❌ Schema drift detected!" | ||
| echo " The live schema (after running all migrations) differs from" | ||
| echo " services/api/database/schema.sql." | ||
| echo "" | ||
| echo " To update the snapshot:" | ||
| echo " pg_dump --schema-only --no-owner --no-privileges --no-comments \\" | ||
| echo " --schema=public <db> > services/api/database/schema.sql" | ||
| echo " git add services/api/database/schema.sql" | ||
| echo " git commit -m 'chore: update schema snapshot'" | ||
| exit 1 | ||
| fi | ||
| echo "✅ Schema matches snapshot — no drift detected." | ||
| - name: Upload live schema dump | ||
| if: always() | ||
| uses: actions/upload-artifact@v4 | ||
| with: | ||
| name: live-schema-dump | ||
| path: /tmp/live-schema.sql | ||
| retention-days: 7 | ||
| all-tests-passed: | ||
| name: All Tests Passed | ||
| needs: | ||
| - prometheus-rules-test | ||
| - unit-tests | ||
| - integration-tests | ||
| - api-rate-limit-tests | ||
| - gas-benchmarks | ||
| - backend-coverage | ||
| - security-audit | ||
| - sast-scanning | ||
| - secrets-scanning | ||
| - dependency-scanning | ||
| - codeql-analysis | ||
| - clippy | ||
| - oracle-quality-gate | ||
| - format | ||
| - build-optimized | ||
| - api-cache-tests | ||
| - e2e-market-creation | ||
| - e2e-mocked | ||
| - e2e-visual-regression | ||
| - bundle-size | ||
| - documentation-sync | ||
| - validate-migration-rollbacks | ||
| - rollback-idempotency | ||
| - api-criterion-benchmarks | ||
| - frontend-unit-coverage | ||
| - schema-drift-check | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - name: Success | ||
| run: echo "All tests passed successfully!" | ||
| documentation-sync: | ||
| name: Documentation Sync Check | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| - name: Setup Node.js | ||
| uses: actions/setup-node@v3 | ||
| with: | ||
| node-version: "18" | ||
| - name: Check API_SPEC.md is in sync with openapi.yaml | ||
| run: node scripts/generate-api-spec.js --check | ||
| - name: Fail if out of sync | ||
| if: failure() | ||
| run: | | ||
| echo "❌ API_SPEC.md is out of sync with openapi.yaml" | ||
| echo "" | ||
| echo "To fix this, run:" | ||
| echo " node scripts/generate-api-spec.js" | ||
| echo " git add API_SPEC.md" | ||
| echo " git commit -m 'chore: regenerate API_SPEC.md'" | ||
| exit 1 | ||