Merge branch 'main' into feat/62-contract-version-tag #9
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: CI | |
| on: | |
| push: | |
| pull_request: | |
| # Run gas estimation weekly on Sundays at 02:00 UTC | |
| schedule: | |
| - cron: "0 2 * * 0" | |
| # Allow manual trigger (e.g., to run gas estimation or load tests on demand) | |
| workflow_dispatch: | |
| jobs: | |
| contract: | |
| name: Rust contract | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Install Rust stable + wasm32 target | |
| uses: dtolnay/rust-toolchain@stable | |
| with: | |
| targets: wasm32-unknown-unknown | |
| - name: Cache Rust dependencies | |
| uses: Swatinem/rust-cache@v2 | |
| with: | |
| workspaces: contracts/subscription | |
| - name: Build (wasm32 release) | |
| run: cargo build --manifest-path contracts/subscription/Cargo.toml --target wasm32-unknown-unknown --release | |
| - name: Test & summary | |
| run: | | |
| set -o pipefail | |
| cargo test --manifest-path contracts/subscription/Cargo.toml 2>&1 \ | |
| | tee /tmp/contract-test-output.txt | |
| echo "--- Test summary ---" | |
| grep -E "^test result|FAILED|ok$" /tmp/contract-test-output.txt || true | |
| # Issue #432 – 95% coverage threshold enforcement | |
| contract-coverage: | |
| name: Rust contract coverage (≥95%) | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Install Rust stable + llvm-tools | |
| uses: dtolnay/rust-toolchain@stable | |
| with: | |
| components: llvm-tools-preview | |
| - name: Cache Rust dependencies | |
| uses: Swatinem/rust-cache@v2 | |
| with: | |
| workspaces: contracts/subscription | |
| - name: Install cargo-llvm-cov | |
| run: cargo install cargo-llvm-cov --locked | |
| - name: Generate coverage report (LCOV + HTML) | |
| run: make test-coverage | |
| - name: Check 95% line coverage threshold | |
| run: | | |
| # Extract line coverage % from lcov.info and fail if below 95 | |
| LCOV_FILE="contracts/target/lcov.info" | |
| if [ ! -f "$LCOV_FILE" ]; then | |
| echo "ERROR: lcov.info not found at $LCOV_FILE" >&2 | |
| exit 1 | |
| fi | |
| LINES_FOUND=$(grep -E "^LF:" "$LCOV_FILE" | awk -F: '{sum += $2} END {print sum}') | |
| LINES_HIT=$(grep -E "^LH:" "$LCOV_FILE" | awk -F: '{sum += $2} END {print sum}') | |
| if [ -z "$LINES_FOUND" ] || [ "$LINES_FOUND" -eq 0 ]; then | |
| echo "ERROR: No coverage data found in lcov.info" >&2 | |
| exit 1 | |
| fi | |
| COVERAGE=$(echo "scale=2; $LINES_HIT * 100 / $LINES_FOUND" | bc) | |
| echo "Line coverage: ${COVERAGE}% (${LINES_HIT}/${LINES_FOUND} lines)" | |
| # Use awk for floating-point comparison (bc outputs e.g. "97.42") | |
| PASS=$(echo "$COVERAGE" | awk '{print ($1 >= 95) ? "yes" : "no"}') | |
| if [ "$PASS" != "yes" ]; then | |
| echo "FAIL: Coverage ${COVERAGE}% is below the required 95% threshold." >&2 | |
| exit 1 | |
| fi | |
| echo "PASS: Coverage ${COVERAGE}% meets the 95% threshold." | |
| - name: Upload LCOV to Codecov | |
| uses: codecov/codecov-action@v4 | |
| with: | |
| files: contracts/target/lcov.info | |
| flags: contract | |
| name: sorobanpay-contract | |
| fail_ci_if_error: false | |
| env: | |
| CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} | |
| - name: Upload HTML report as artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: contract-coverage-html | |
| path: contracts/target/coverage-html/ | |
| - name: Upload LCOV report as artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: contract-coverage-lcov | |
| path: contracts/target/lcov.info | |
| # Issue #431 – Integration test suite on local Soroban node | |
| contract-integration: | |
| name: Smart contract integration tests | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Install Rust stable + wasm32 target | |
| uses: dtolnay/rust-toolchain@stable | |
| with: | |
| targets: wasm32-unknown-unknown | |
| - name: Cache Rust dependencies | |
| uses: Swatinem/rust-cache@v2 | |
| with: | |
| workspaces: contracts/subscription | |
| - name: Install Stellar CLI | |
| run: cargo install --locked stellar-cli --features opt | |
| - name: Make integration test script executable | |
| run: chmod +x scripts/integration-test.sh | |
| - name: Run integration tests (local Soroban node via Docker) | |
| run: bash scripts/integration-test.sh | |
| env: | |
| DOCKER_BUILDKIT: 1 | |
| benchmark: | |
| name: Resource benchmark (testnet) | |
| runs-on: ubuntu-latest | |
| # Run only when contract files change, or on demand. | |
| # Does NOT block merges — regressions emit warnings only. | |
| if: > | |
| github.event_name == 'workflow_dispatch' || | |
| contains(join(github.event.commits.*.modified, ','), 'contracts/') | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Install Rust stable + wasm32 target | |
| uses: dtolnay/rust-toolchain@stable | |
| with: | |
| targets: wasm32-unknown-unknown | |
| - name: Cache Rust dependencies | |
| uses: Swatinem/rust-cache@v2 | |
| with: | |
| workspaces: contracts/subscription | |
| - name: Install Stellar CLI | |
| run: cargo install --locked stellar-cli --features opt | |
| - name: Install jq | |
| run: sudo apt-get install -y jq | |
| - name: Generate or fund testnet identity | |
| env: | |
| STELLAR_IDENTITY: ci-benchmark | |
| run: | | |
| stellar keys generate "$STELLAR_IDENTITY" --network testnet --overwrite || true | |
| stellar keys fund "$STELLAR_IDENTITY" --network testnet || true | |
| - name: Run benchmark (deploy + simulate all entry points) | |
| id: benchmark | |
| env: | |
| STELLAR_IDENTITY: ci-benchmark | |
| STELLAR_NETWORK: testnet | |
| OUTPUT_JSON: /tmp/benchmark-current.json | |
| OUTPUT_MD: /tmp/performance-current.md | |
| run: bash scripts/benchmark.sh | |
| continue-on-error: true # Network issues must not block CI | |
| - name: Check for regressions against baseline | |
| if: steps.benchmark.outcome == 'success' | |
| env: | |
| BASELINE_PATH: docs/performance-baseline.json | |
| CURRENT_PATH: /tmp/benchmark-current.json | |
| run: bash scripts/check_regression.sh | |
| # Exit 0 always — regressions are warnings, not failures | |
| - name: Upload benchmark results as artifact | |
| if: steps.benchmark.outcome == 'success' | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: benchmark-results-${{ github.sha }} | |
| path: | | |
| /tmp/benchmark-current.json | |
| /tmp/performance-current.md | |
| retention-days: 30 | |
| - name: Warn if benchmark step failed | |
| if: steps.benchmark.outcome != 'success' | |
| run: | | |
| echo "::warning::Benchmark step failed (likely a testnet connectivity issue). Resource regression check was skipped. Re-run the workflow or check testnet availability." | |
| backend-integration: | |
| name: Backend integration tests | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: 20 | |
| cache: npm | |
| cache-dependency-path: backend/package-lock.json | |
| - name: Install dependencies | |
| run: npm ci | |
| working-directory: backend | |
| - name: Run integration tests | |
| run: npm run test:integration | |
| working-directory: backend | |
| # Issue #433 – Frontend unit tests + coverage (>80%) | |
| frontend: | |
| name: Frontend type check, unit tests & coverage | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: 20 | |
| cache: npm | |
| cache-dependency-path: frontend/package-lock.json | |
| - name: Install dependencies | |
| run: npm ci | |
| working-directory: frontend | |
| - name: Type check | |
| run: npm run type-check | |
| working-directory: frontend | |
| - name: Run unit tests with coverage | |
| run: npm run test:ci | |
| working-directory: frontend | |
| - name: Upload frontend coverage to Codecov | |
| uses: codecov/codecov-action@v4 | |
| with: | |
| files: frontend/coverage/lcov.info | |
| flags: frontend | |
| name: sorobanpay-frontend | |
| fail_ci_if_error: false | |
| env: | |
| CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} | |
| - name: Upload coverage HTML report | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: frontend-coverage-html | |
| path: frontend/coverage/ | |
| frontend-typecheck: | |
| name: Frontend strict type check | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: 20 | |
| cache: npm | |
| cache-dependency-path: frontend/package-lock.json | |
| - name: Install dependencies | |
| run: npm ci | |
| working-directory: frontend | |
| - name: Type check (fail on errors) | |
| run: npm run type-check | |
| working-directory: frontend | |
| # FE-48: E2E tests with Playwright for critical user flows | |
| frontend-e2e: | |
| name: Frontend E2E tests (Playwright) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: 20 | |
| cache: npm | |
| cache-dependency-path: frontend/package-lock.json | |
| - name: Install dependencies | |
| run: npm ci | |
| working-directory: frontend | |
| - name: Install Playwright browsers | |
| run: npx playwright install --with-deps chromium | |
| working-directory: frontend | |
| - name: Run E2E tests | |
| run: npx playwright test --project=chromium | |
| working-directory: frontend | |
| env: | |
| CI: "true" | |
| NEXT_PUBLIC_CONTRACT_ID: CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM | |
| - name: Upload Playwright report on failure | |
| if: failure() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: playwright-report | |
| path: frontend/playwright-report/ | |
| retention-days: 7 | |
| - name: Upload Playwright test results on failure | |
| if: failure() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: playwright-results | |
| path: frontend/playwright-results/ | |
| retention-days: 7 | |
| # TEST-102: k6 load tests — runs weekly against staging environment | |
| load-test: | |
| name: "[TEST-102] k6 load tests" | |
| runs-on: ubuntu-latest | |
| # Only run on schedule or manual trigger; requires a staging environment | |
| if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Install k6 | |
| run: | | |
| sudo gpg -k | |
| sudo gpg --no-default-keyring \ | |
| --keyring /usr/share/keyrings/k6-archive-keyring.gpg \ | |
| --keyserver hkp://keyserver.ubuntu.com:80 \ | |
| --recv-keys C5AD17C747E3415A3642D57D77C6C491D6AC1D69 | |
| echo "deb [signed-by=/usr/share/keyrings/k6-archive-keyring.gpg] https://dl.k6.io/deb stable main" \ | |
| | sudo tee /etc/apt/sources.list.d/k6.list | |
| sudo apt-get update && sudo apt-get install -y k6 | |
| - name: Create results directory | |
| run: mkdir -p results | |
| - name: "[TEST-102] Read-heavy scenario (100 VUs, 2 min)" | |
| env: | |
| BASE_URL: ${{ secrets.LOAD_TEST_BASE_URL }} | |
| MERCHANT_ADDRESS: ${{ secrets.LOAD_TEST_MERCHANT_ADDRESS }} | |
| run: | | |
| k6 run \ | |
| -e BASE_URL="$BASE_URL" \ | |
| -e MERCHANT_ADDRESS="$MERCHANT_ADDRESS" \ | |
| --out json=results/read-heavy-results.json \ | |
| tests/load/read-heavy.js | |
| - name: "[TEST-102] Mixed scenario (50 VUs, ramp)" | |
| env: | |
| BASE_URL: ${{ secrets.LOAD_TEST_BASE_URL }} | |
| MERCHANT_ADDRESS: ${{ secrets.LOAD_TEST_MERCHANT_ADDRESS }} | |
| run: | | |
| k6 run \ | |
| -e BASE_URL="$BASE_URL" \ | |
| -e MERCHANT_ADDRESS="$MERCHANT_ADDRESS" \ | |
| --out json=results/mixed-results.json \ | |
| tests/load/mixed.js | |
| - name: "[TEST-102] Webhook storm scenario (50 VUs, 2 min)" | |
| env: | |
| BASE_URL: ${{ secrets.LOAD_TEST_BASE_URL }} | |
| MERCHANT_ADDRESS: ${{ secrets.LOAD_TEST_MERCHANT_ADDRESS }} | |
| run: | | |
| k6 run \ | |
| -e BASE_URL="$BASE_URL" \ | |
| -e MERCHANT_ADDRESS="$MERCHANT_ADDRESS" \ | |
| --out json=results/webhook-storm-results.json \ | |
| tests/load/webhook-storm.js | |
| - name: Upload load test results | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: load-test-results | |
| path: results/ | |
| # TEST-100: Gas estimation regression harness — runs against staging on schedule | |
| gas-estimation: | |
| name: "[TEST-100] Gas estimation regression" | |
| runs-on: ubuntu-latest | |
| # Only run on schedule or when manually triggered; staging credentials required | |
| if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: 20 | |
| - name: Install script dependencies | |
| run: npm install | |
| working-directory: . | |
| - name: Run gas estimation harness | |
| env: | |
| RPC_URL: ${{ secrets.GAS_RPC_URL }} | |
| CONTRACT_ID: ${{ secrets.GAS_CONTRACT_ID }} | |
| SUBSCRIBER_KEY: ${{ secrets.GAS_SUBSCRIBER_KEY }} | |
| MERCHANT_KEY: ${{ secrets.GAS_MERCHANT_KEY }} | |
| TOKEN_ADDRESS: ${{ secrets.GAS_TOKEN_ADDRESS }} | |
| NETWORK_PASSPHRASE: ${{ secrets.GAS_NETWORK_PASSPHRASE }} | |
| run: npm run gas-estimation | |
| - name: Upload gas estimation report | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: gas-estimation-report | |
| path: gas-estimation-report.md | |
| # Issue #458 OPS-123 — Automated contract deployment with version pinning | |
| deploy: | |
| name: Deploy contract & update manifest | |
| runs-on: ubuntu-latest | |
| # Only run on direct pushes to main — not on PRs, schedules, or other branches | |
| if: github.event_name == 'push' && github.ref == 'refs/heads/main' | |
| # Ensure contract tests pass before deploying | |
| needs: [contract] | |
| permissions: | |
| contents: write # required to commit deployments.json back to the repo | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| # Use a PAT or the default GITHUB_TOKEN. A PAT is needed if branch | |
| # protection rules prevent the default token from pushing to main. | |
| token: ${{ secrets.DEPLOY_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} | |
| # Fetch full history so `git rev-parse HEAD` is always accurate | |
| fetch-depth: 0 | |
| - name: Install Rust stable + wasm32 target | |
| uses: dtolnay/rust-toolchain@stable | |
| with: | |
| targets: wasm32-unknown-unknown | |
| - name: Cache Rust dependencies | |
| uses: Swatinem/rust-cache@v2 | |
| with: | |
| workspaces: contracts/subscription | |
| # Pin Stellar CLI to the exact version recorded in deploy.sh | |
| - name: Install Stellar CLI (pinned) | |
| run: | | |
| STELLAR_CLI_VERSION="21.3.0" | |
| echo "Installing stellar-cli ${STELLAR_CLI_VERSION}..." | |
| cargo install --locked stellar-cli \ | |
| --version "${STELLAR_CLI_VERSION}" \ | |
| --features opt | |
| stellar version | |
| # Import the deployer secret key as a named identity so deploy.sh can sign | |
| - name: Configure Stellar identity | |
| env: | |
| STELLAR_SECRET_KEY: ${{ secrets.STELLAR_SECRET_KEY }} | |
| run: | | |
| stellar keys add deployer --secret-key "$STELLAR_SECRET_KEY" | |
| - name: Deploy contract | |
| id: deploy | |
| env: | |
| STELLAR_NETWORK: ${{ vars.STELLAR_NETWORK || 'testnet' }} | |
| STELLAR_IDENTITY: deployer | |
| run: | | |
| chmod +x deploy/deploy.sh | |
| CONTRACT_ID=$(bash deploy/deploy.sh) | |
| echo "contract_id=${CONTRACT_ID}" >> "$GITHUB_OUTPUT" | |
| echo "Deployed contract: ${CONTRACT_ID}" | |
| - name: Commit & push deployments.json | |
| env: | |
| CONTRACT_ID: ${{ steps.deploy.outputs.contract_id }} | |
| run: | | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| # Stage only the manifest — nothing else | |
| git add deploy/deployments.json | |
| # Skip commit if the manifest is unchanged (idempotent re-runs) | |
| if git diff --cached --quiet; then | |
| echo "deployments.json unchanged — nothing to commit." | |
| exit 0 | |
| fi | |
| NETWORK="${STELLAR_NETWORK:-testnet}" | |
| git commit -m "chore(deploy): update deployments.json [${NETWORK}] ${CONTRACT_ID:0:12} [skip ci]" | |
| git push |