Skip to content

CI/CD Pipeline

CI/CD Pipeline #518

Workflow file for this run

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 }}'