CI/CD Pipeline #518
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/CD Pipeline | |
| on: | |
| push: | |
| branches: [main, v2, develop] | |
| pull_request: | |
| branches: [main, v2] | |
| schedule: | |
| # Run daily security scan at 02:00 UTC | |
| - cron: '0 2 * * *' | |
| env: | |
| NODE_VERSION: '20' | |
| FOUNDRY_PROFILE: 'ci' | |
| jobs: | |
| setup: | |
| name: Setup and Validate | |
| runs-on: ubuntu-latest | |
| outputs: | |
| packages: ${{ steps.detect-packages.outputs.packages }} | |
| cache-key: ${{ steps.generate-cache-key.outputs.cache-key }} | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| cache: 'npm' | |
| - name: Install jq for JSON processing | |
| run: sudo apt-get update && sudo apt-get install -y jq | |
| - name: Detect packages | |
| id: detect-packages | |
| run: | | |
| packages=$(find packages -name "package.json" -type f | jq -R -s -c 'split("\n")[:-1] | map(split("/")[1])') | |
| echo "packages=${packages}" >> $GITHUB_OUTPUT | |
| echo "Detected packages: ${packages}" | |
| - name: Generate cache key | |
| id: generate-cache-key | |
| run: | | |
| cache_key="${{ runner.os }}-deps-${{ hashFiles('**/package-lock.json') }}" | |
| echo "cache-key=${cache_key}" >> $GITHUB_OUTPUT | |
| echo "Generated cache key: ${cache_key}" | |
| - name: Cache dependencies | |
| id: cache-deps | |
| uses: actions/cache@v3 | |
| with: | |
| path: | | |
| node_modules | |
| packages/*/node_modules | |
| ~/.npm | |
| key: ${{ steps.generate-cache-key.outputs.cache-key }} | |
| restore-keys: | | |
| ${{ runner.os }}-deps- | |
| - name: Install root dependencies | |
| run: npm ci | |
| - name: Install package dependencies | |
| run: | | |
| for package in $(echo '${{ steps.detect-packages.outputs.packages }}' | jq -r '.[]'); do | |
| if [ -d "packages/$package" ]; then | |
| echo "Installing dependencies for $package" | |
| cd "packages/$package" | |
| npm ci | |
| cd "$GITHUB_WORKSPACE" | |
| fi | |
| done | |
| type-check: | |
| name: TypeScript Type Checking | |
| runs-on: ubuntu-latest | |
| needs: setup | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| package: ${{ fromJson(needs.setup.outputs.packages) }} | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| cache: 'npm' | |
| - name: Restore dependencies | |
| uses: actions/cache@v3 | |
| with: | |
| path: | | |
| node_modules | |
| packages/*/node_modules | |
| ~/.npm | |
| key: ${{ needs.setup.outputs.cache-key }} | |
| - name: Install dependencies (workspace) | |
| run: npm ci | |
| - name: Type check ${{ matrix.package }} | |
| run: | | |
| cd packages/${{ matrix.package }} | |
| if [ -d "../sdk" ] && [ "${{ matrix.package }}" = "proxy-server" ]; then | |
| echo "Building SDK before proxy-server type-check..." | |
| cd ../sdk && npm run build && cd ../proxy-server | |
| fi | |
| npm run type-check | |
| - name: Check for unsafe type usage | |
| run: | | |
| echo "Checking for unsafe type usage in ${{ matrix.package }}..." | |
| cd packages/${{ matrix.package }}/src | |
| # Check for 'as any' usage | |
| if grep -r "as any" . --include="*.ts" --exclude-dir=test; then | |
| echo "::warning::Found 'as any' usage in ${{ matrix.package }}" | |
| fi | |
| # Check for ': any' usage | |
| if grep -r ": any" . --include="*.ts" --exclude-dir=test; then | |
| echo "::warning::Found ': any' usage in ${{ matrix.package }}" | |
| fi | |
| # Check for @ts-ignore | |
| if grep -r "@ts-ignore" . --include="*.ts"; then | |
| echo "::error::Found @ts-ignore in ${{ matrix.package }}" | |
| exit 1 | |
| fi | |
| build: | |
| name: Build Packages | |
| runs-on: ubuntu-latest | |
| needs: [setup, type-check] | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| package: ${{ fromJson(needs.setup.outputs.packages) }} | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| cache: 'npm' | |
| - name: Restore dependencies | |
| uses: actions/cache@v3 | |
| with: | |
| path: | | |
| node_modules | |
| packages/*/node_modules | |
| ~/.npm | |
| key: ${{ needs.setup.outputs.cache-key }} | |
| - name: Install dependencies (workspace) | |
| run: npm ci | |
| - name: Install Foundry (contracts build only) | |
| if: matrix.package == 'contracts' | |
| uses: foundry-rs/foundry-toolchain@v1 | |
| with: | |
| version: stable | |
| - name: Prepare contracts deps (contracts build only) | |
| if: matrix.package == 'contracts' | |
| run: | | |
| cd packages/contracts | |
| forge install | |
| - name: Build ${{ matrix.package }} | |
| run: | | |
| if [ "${{ matrix.package }}" = "sdk" ]; then | |
| cd packages/sdk | |
| # Use ts-node loader if rollup config is ESM TS; ignore if not needed | |
| if [ -f rollup.config.ts ]; then echo "rollup config is TS"; fi | |
| npm run build | |
| elif [ "${{ matrix.package }}" = "proxy-server" ]; then | |
| cd packages/sdk && npm run build && cd ../proxy-server | |
| npm run build | |
| else | |
| cd packages/${{ matrix.package }} | |
| npm run build | |
| fi | |
| - name: Upload build artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: build-${{ matrix.package }} | |
| path: packages/${{ matrix.package }}/dist/ | |
| retention-days: 7 | |
| test-unit: | |
| name: Unit Tests | |
| runs-on: ubuntu-latest | |
| needs: [setup, build] | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| package: ${{ fromJson(needs.setup.outputs.packages) }} | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| cache: 'npm' | |
| - name: Restore dependencies | |
| uses: actions/cache@v3 | |
| with: | |
| path: | | |
| node_modules | |
| packages/*/node_modules | |
| ~/.npm | |
| key: ${{ needs.setup.outputs.cache-key }} | |
| - name: Install dependencies (workspace) | |
| run: npm ci | |
| - name: Download build artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: build-${{ matrix.package }} | |
| path: packages/${{ matrix.package }}/dist/ | |
| - name: Run unit tests for ${{ matrix.package }} | |
| run: | | |
| cd packages/${{ matrix.package }} | |
| if npm run test --if-present; then | |
| echo "Tests passed for ${{ matrix.package }}" | |
| else | |
| echo "Tests failed or not configured for ${{ matrix.package }}" | |
| fi | |
| - name: Upload test coverage | |
| if: success() && matrix.package == 'sdk' | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: coverage-${{ matrix.package }} | |
| path: packages/${{ matrix.package }}/coverage/ | |
| retention-days: 7 | |
| test-contracts: | |
| name: Smart Contract Tests | |
| runs-on: ubuntu-latest | |
| needs: setup | |
| if: contains(fromJson(needs.setup.outputs.packages), 'contracts') | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| with: | |
| submodules: recursive | |
| - name: Install Foundry | |
| uses: foundry-rs/foundry-toolchain@v1 | |
| with: | |
| version: stable | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| cache: 'npm' | |
| - name: Restore dependencies | |
| uses: actions/cache@v3 | |
| with: | |
| path: | | |
| node_modules | |
| packages/*/node_modules | |
| ~/.npm | |
| key: ${{ needs.setup.outputs.cache-key }} | |
| - name: Install contract dependencies | |
| run: | | |
| cd packages/contracts | |
| forge install | |
| - name: Run contract tests | |
| run: | | |
| cd packages/contracts | |
| forge test -vvv || true | |
| - name: Generate gas report | |
| run: | | |
| cd packages/contracts | |
| forge test --gas-report > gas-report.txt || true | |
| - name: Upload gas report | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: gas-report | |
| path: packages/contracts/gas-report.txt | |
| retention-days: 30 | |
| test-integration: | |
| name: Integration Tests | |
| runs-on: ubuntu-latest | |
| needs: [setup, build] | |
| if: github.event_name != 'schedule' | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| cache: 'npm' | |
| - name: Install Foundry (for local blockchain) | |
| uses: foundry-rs/foundry-toolchain@v1 | |
| with: | |
| version: stable | |
| - name: Restore dependencies | |
| uses: actions/cache@v3 | |
| with: | |
| path: | | |
| node_modules | |
| packages/*/node_modules | |
| ~/.npm | |
| key: ${{ needs.setup.outputs.cache-key }} | |
| - name: Install dependencies (workspace) | |
| run: npm ci | |
| - name: Download build artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: build-sdk | |
| path: packages/sdk/dist/ | |
| - name: Download proxy build | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: build-proxy-server | |
| path: packages/proxy-server/dist/ | |
| - name: Start local blockchain (Anvil) | |
| run: | | |
| anvil --host 0.0.0.0 --port 8545 & | |
| ANVIL_PID=$! | |
| for i in {1..20}; do | |
| if curl -s http://localhost:8545 >/dev/null 2>&1; then | |
| echo "Anvil is up"; break; fi; sleep 1; done | |
| if ! curl -s http://localhost:8545 >/dev/null 2>&1; then echo "Anvil failed to start"; exit 1; fi | |
| - name: Run integration tests | |
| env: | |
| BLOBKIT_RPC_URL: 'http://localhost:8545' | |
| BLOBKIT_CHAIN_ID: '31337' | |
| BLOBKIT_ESCROW_31337: '0x1234567890123456789012345678901234567890' | |
| BLOBKIT_LOG_LEVEL: 'silent' | |
| run: | | |
| cd test | |
| npm install | |
| npm test | |
| security-audit: | |
| name: Security Audit | |
| runs-on: ubuntu-latest | |
| needs: setup | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| cache: 'npm' | |
| - name: Restore dependencies | |
| uses: actions/cache@v3 | |
| with: | |
| path: | | |
| node_modules | |
| packages/*/node_modules | |
| ~/.npm | |
| key: ${{ needs.setup.outputs.cache-key }} | |
| - name: Install dependencies (workspace) | |
| run: npm ci | |
| - name: Run npm audit | |
| run: | | |
| for package in $(echo '${{ needs.setup.outputs.packages }}' | jq -r '.[]'); do | |
| echo "Running npm audit for $package" | |
| cd "packages/$package" | |
| npm audit --audit-level high || echo "Vulnerabilities found in $package" | |
| cd "$GITHUB_WORKSPACE" | |
| done | |
| - name: Install and run Slither (if contracts exist) | |
| if: contains(fromJson(needs.setup.outputs.packages), 'contracts') | |
| run: | | |
| pip3 install slither-analyzer | |
| cd packages/contracts | |
| slither . --json slither-report.json || true | |
| - name: Upload security reports | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: security-reports | |
| path: | | |
| packages/contracts/slither-report.json | |
| retention-days: 30 | |
| performance-test: | |
| name: Performance Tests | |
| runs-on: ubuntu-latest | |
| needs: [setup, build] | |
| if: github.event_name != 'schedule' | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| cache: 'npm' | |
| - name: Install autocannon for load testing | |
| run: npm install -g autocannon | |
| - name: Restore dependencies | |
| uses: actions/cache@v3 | |
| with: | |
| path: | | |
| node_modules | |
| packages/*/node_modules | |
| ~/.npm | |
| key: ${{ needs.setup.outputs.cache-key }} | |
| - name: Install dependencies (workspace) | |
| run: npm ci | |
| - name: Download proxy build | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: build-proxy-server | |
| path: packages/proxy-server/dist/ | |
| - name: Start proxy server | |
| env: | |
| PORT: 3001 | |
| RPC_URL: 'http://localhost:8545' | |
| CHAIN_ID: '31337' | |
| ESCROW_CONTRACT: '0x1234567890123456789012345678901234567890' | |
| LOG_LEVEL: 'error' | |
| run: | | |
| cd packages/proxy-server | |
| timeout 30 node dist/index.js & | |
| sleep 5 | |
| - name: Run performance tests | |
| run: | | |
| echo "Running load test on health endpoint..." | |
| autocannon -c 10 -d 5 http://localhost:3001/api/v1/health | |
| - name: SDK Performance Test | |
| run: | | |
| echo "Testing SDK performance..." | |
| node -e " | |
| const start = Date.now(); | |
| for(let i = 0; i < 1000; i++) { | |
| const crypto = require('crypto'); | |
| crypto.createHash('sha256').update('test' + i).digest('hex'); | |
| } | |
| const duration = Date.now() - start; | |
| console.log('1000 hash operations completed in', duration, 'ms'); | |
| if (duration > 1000) { | |
| console.log('::warning::Performance slower than expected'); | |
| } | |
| " | |
| package-verification: | |
| name: Package Export Verification | |
| runs-on: ubuntu-latest | |
| needs: [setup, build] | |
| strategy: | |
| matrix: | |
| package: ${{ fromJson(needs.setup.outputs.packages) }} | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| cache: 'npm' | |
| - name: Restore dependencies | |
| uses: actions/cache@v3 | |
| with: | |
| path: | | |
| node_modules | |
| packages/*/node_modules | |
| ~/.npm | |
| key: ${{ needs.setup.outputs.cache-key }} | |
| - name: Install dependencies (workspace) | |
| run: npm ci | |
| - name: Download build artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: build-${{ matrix.package }} | |
| path: packages/${{ matrix.package }}/dist/ | |
| - name: Verify package exports | |
| run: | | |
| cd packages/${{ matrix.package }} | |
| npm pack --dry-run | |
| - name: Test SDK import (SDK only) | |
| if: matrix.package == 'sdk' | |
| env: | |
| BLOBKIT_ESCROW_31337: '0x1234567890123456789012345678901234567890' | |
| run: | | |
| cd packages/sdk | |
| node -e " | |
| try { | |
| const { BlobKit, createFromEnv } = require('./dist/index.cjs'); | |
| console.log('✓ SDK import successful'); | |
| console.log('✓ BlobKit class available:', typeof BlobKit); | |
| console.log('✓ createFromEnv function available:', typeof createFromEnv); | |
| // Test basic instantiation | |
| const blobkit = new BlobKit({ | |
| rpcUrl: 'http://localhost:8545', | |
| chainId: 31337, | |
| escrowContract: '0x1234567890123456789012345678901234567890' | |
| }); | |
| console.log('✓ BlobKit instantiation successful'); | |
| } catch (error) { | |
| console.error('✗ SDK import failed:', error.message); | |
| process.exit(1); | |
| } | |
| " | |
| production-readiness: | |
| name: Production Readiness Check | |
| runs-on: ubuntu-latest | |
| needs: | |
| [setup, type-check, build, test-unit, test-contracts, security-audit, package-verification] | |
| if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/v2' | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| cache: 'npm' | |
| - name: Install jq | |
| run: sudo apt-get update && sudo apt-get install -y jq | |
| - name: Restore dependencies | |
| uses: actions/cache@v3 | |
| with: | |
| path: | | |
| node_modules | |
| packages/*/node_modules | |
| ~/.npm | |
| key: ${{ needs.setup.outputs.cache-key }} | |
| - name: Install dependencies (workspace) | |
| run: npm ci | |
| - name: Download all build artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| path: artifacts/ | |
| - name: Restore build artifacts | |
| run: | | |
| for package in sdk proxy-server contracts; do | |
| if [ -d "artifacts/build-$package" ]; then | |
| mkdir -p "packages/$package/dist" | |
| cp -r "artifacts/build-$package/"* "packages/$package/dist/" | |
| fi | |
| done | |
| - name: Run production readiness check | |
| run: | | |
| if [ -f ./production-check.sh ]; then | |
| chmod +x ./production-check.sh | |
| ./production-check.sh | |
| else | |
| echo "production-check.sh not found, skipping." | |
| fi | |
| - name: Upload production check results | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: production-check-results | |
| path: | | |
| production-check-results.json | |
| production-check.log | |
| .production-status | |
| retention-days: 30 | |
| - name: Set production status | |
| if: always() | |
| run: | | |
| if [ -f ".production-status" ]; then | |
| status=$(cat .production-status | cut -d'=' -f2) | |
| echo "PRODUCTION_STATUS=$status" >> $GITHUB_ENV | |
| echo "Production status: $status" | |
| fi | |
| push-image: | |
| name: push the image to the container registry | |
| if: (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/v2') | |
| runs-on: ubuntu-latest | |
| permissions: | |
| id-token: write | |
| contents: read | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Google Auth | |
| id: auth | |
| uses: 'google-github-actions/auth@v2' | |
| with: | |
| token_format: 'access_token' | |
| workload_identity_provider: '${{ secrets.WIF_PROVIDER_ID }}' | |
| service_account: '${{ secrets.SERVICE_ACCOUNT_EMAIL }}' | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v3 | |
| - name: Docker Auth | |
| id: docker-auth | |
| uses: 'docker/login-action@v3' | |
| with: | |
| username: 'oauth2accesstoken' | |
| password: '${{ steps.auth.outputs.access_token }}' | |
| registry: 'us-central1-docker.pkg.dev' | |
| - name: Build, tag and push container | |
| id: build-image | |
| uses: docker/build-push-action@v5 | |
| with: | |
| context: . | |
| push: true | |
| cache-from: type=gha | |
| cache-to: type=gha,mode=max | |
| tags: | | |
| us-central1-docker.pkg.dev/${{ secrets.GCP_PROJECT }}/blobkit/app:${{ github.sha }} | |
| us-central1-docker.pkg.dev/${{ secrets.GCP_PROJECT }}/blobkit/app:latest | |
| deploy-proxy-server: | |
| if: (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/v2') | |
| name: Deploy to production | |
| runs-on: ubuntu-latest | |
| needs: [push-image, production-readiness] | |
| permissions: | |
| id-token: write | |
| contents: read | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Set up Helm | |
| uses: azure/setup-helm@v4.3.0 | |
| with: | |
| version: v3.16.1 | |
| - name: Google Auth | |
| id: auth | |
| uses: 'google-github-actions/auth@v2' | |
| with: | |
| token_format: 'access_token' | |
| workload_identity_provider: '${{ secrets.WIF_PROVIDER_ID }}' | |
| service_account: '${{ secrets.SERVICE_ACCOUNT_EMAIL }}' | |
| - name: 'Set up Cloud SDK' | |
| uses: 'google-github-actions/setup-gcloud@v2' | |
| with: | |
| version: '>=494.0.0' | |
| - name: 'Install kubectl' | |
| run: 'gcloud components install kubectl' | |
| - name: 'Get cluster credentials' | |
| run: 'gcloud container clusters get-credentials ${{ secrets.CLUSTER_NAME }} --region ${{ secrets.REGION }}' | |
| - name: 'Deploy' | |
| run: 'helm upgrade --install blobkit-proxy ./ops/chart --set version=${{ github.sha }} --set image=us-central1-docker.pkg.dev/${{ secrets.GCP_PROJECT }}/blobkit/app --set projectId=${{ secrets.GCP_PROJECT }}' | |