Retry EC2 deployment #11
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: CD | |
| on: | |
| push: | |
| branches: [main] | |
| workflow_dispatch: | |
| env: | |
| REGISTRY: ghcr.io | |
| IMAGE_PREFIX: ${{ vars.IMAGE_PREFIX || 'ghcr.io/shikhar-395/exchange' }} | |
| jobs: | |
| changes: | |
| runs-on: ubuntu-latest | |
| outputs: | |
| backend: ${{ steps.filter.outputs.backend }} | |
| engine: ${{ steps.filter.outputs.engine }} | |
| db-processor: ${{ steps.filter.outputs.db-processor }} | |
| websocket: ${{ steps.filter.outputs.websocket }} | |
| market-maker: ${{ steps.filter.outputs.market-maker }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: dorny/paths-filter@v3 | |
| id: filter | |
| with: | |
| filters: | | |
| backend: | |
| - '.github/workflows/cd.yml' | |
| - 'apps/backend/**' | |
| - 'packages/database/**' | |
| - 'packages/common/**' | |
| - 'packages/email/**' | |
| - 'docker/backend/**' | |
| - 'docker/compose-files/docker-compose-deploy.yml' | |
| engine: | |
| - '.github/workflows/cd.yml' | |
| - 'apps/engine/**' | |
| - 'packages/database/**' | |
| - 'packages/common/**' | |
| - 'docker/engine/**' | |
| - 'docker/compose-files/docker-compose-deploy.yml' | |
| db-processor: | |
| - '.github/workflows/cd.yml' | |
| - 'apps/db-processor/**' | |
| - 'packages/common/**' | |
| - 'packages/database/**' | |
| - 'docker/db-processor/**' | |
| - 'docker/compose-files/docker-compose-deploy.yml' | |
| websocket: | |
| - '.github/workflows/cd.yml' | |
| - 'apps/websocket/**' | |
| - 'packages/common/**' | |
| - 'docker/websocket/**' | |
| - 'docker/compose-files/docker-compose-deploy.yml' | |
| market-maker: | |
| - '.github/workflows/cd.yml' | |
| - 'apps/market-maker/**' | |
| - 'docker/market-maker/**' | |
| - 'docker/compose-files/docker-compose-deploy.yml' | |
| build-backend: | |
| needs: changes | |
| if: needs.changes.outputs.backend == 'true' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| packages: write | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Log in to GitHub Container Registry | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: ghcr.io | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Build and push backend | |
| uses: docker/build-push-action@v6 | |
| with: | |
| context: . | |
| file: docker/backend/Dockerfile | |
| push: true | |
| tags: | | |
| ${{ env.IMAGE_PREFIX }}/backend:${{ github.sha }} | |
| ${{ env.IMAGE_PREFIX }}/backend:latest | |
| build-engine: | |
| needs: changes | |
| if: needs.changes.outputs.engine == 'true' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| packages: write | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Log in to GitHub Container Registry | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: ghcr.io | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Build and push engine | |
| uses: docker/build-push-action@v6 | |
| with: | |
| context: . | |
| file: docker/engine/Dockerfile | |
| push: true | |
| tags: | | |
| ${{ env.IMAGE_PREFIX }}/engine:${{ github.sha }} | |
| ${{ env.IMAGE_PREFIX }}/engine:latest | |
| build-db-processor: | |
| needs: changes | |
| if: needs.changes.outputs.db-processor == 'true' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| packages: write | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Log in to GitHub Container Registry | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: ghcr.io | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Build and push db-processor | |
| uses: docker/build-push-action@v6 | |
| with: | |
| context: . | |
| file: docker/db-processor/Dockerfile | |
| push: true | |
| tags: | | |
| ${{ env.IMAGE_PREFIX }}/db-processor:${{ github.sha }} | |
| ${{ env.IMAGE_PREFIX }}/db-processor:latest | |
| build-websocket: | |
| needs: changes | |
| if: needs.changes.outputs.websocket == 'true' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| packages: write | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Log in to GitHub Container Registry | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: ghcr.io | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Build and push websocket | |
| uses: docker/build-push-action@v6 | |
| with: | |
| context: . | |
| file: docker/websocket/Dockerfile | |
| push: true | |
| tags: | | |
| ${{ env.IMAGE_PREFIX }}/websocket:${{ github.sha }} | |
| ${{ env.IMAGE_PREFIX }}/websocket:latest | |
| build-market-maker: | |
| needs: changes | |
| if: needs.changes.outputs.market-maker == 'true' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| packages: write | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Log in to GitHub Container Registry | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: ghcr.io | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Build and push market-maker | |
| uses: docker/build-push-action@v6 | |
| with: | |
| context: . | |
| file: docker/market-maker/Dockerfile | |
| push: true | |
| tags: | | |
| ${{ env.IMAGE_PREFIX }}/market-maker:${{ github.sha }} | |
| ${{ env.IMAGE_PREFIX }}/market-maker:latest | |
| deploy: | |
| needs: | |
| - build-backend | |
| - build-engine | |
| - build-db-processor | |
| - build-websocket | |
| - build-market-maker | |
| if: always() && !failure() && !cancelled() | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Copy compose file to VM | |
| uses: appleboy/scp-action@v0.1.7 | |
| with: | |
| host: ${{ secrets.SSH_HOST }} | |
| username: ${{ secrets.SSH_USERNAME }} | |
| key: ${{ secrets.SSH_KEY }} | |
| passphrase: ${{ secrets.SSH_PASSPHRASE }} | |
| source: docker/compose-files/docker-compose-deploy.yml | |
| target: /home/${{ secrets.SSH_USERNAME }}/exchange/ | |
| strip_components: 2 | |
| - name: Deploy stack to VM | |
| uses: appleboy/ssh-action@v1 | |
| with: | |
| host: ${{ secrets.SSH_HOST }} | |
| username: ${{ secrets.SSH_USERNAME }} | |
| key: ${{ secrets.SSH_KEY }} | |
| passphrase: ${{ secrets.SSH_PASSPHRASE }} | |
| script: | | |
| set -euo pipefail | |
| cd /home/${{ secrets.SSH_USERNAME }}/exchange | |
| for file in .env .env.backend .env.engine .env.db-processor .env.websocket .env.market-maker; do | |
| if [ ! -f "$file" ]; then | |
| echo "Missing $file in $(pwd). Create it on the VM before deployment." | |
| exit 1 | |
| fi | |
| done | |
| export IMAGE_PREFIX="${{ env.IMAGE_PREFIX }}" | |
| export IMAGE_TAG="latest" | |
| docker compose -f docker-compose-deploy.yml pull backend engine db-processor websocket market-maker | |
| docker compose -f docker-compose-deploy.yml up -d redis timescaledb backend engine db-processor websocket market-maker | |
| # Wait for TimescaleDB before letting schema setup run | |
| until docker exec timescaledb pg_isready -U exchange; do sleep 2; done | |
| migrate: | |
| needs: deploy | |
| if: always() && !failure() && !cancelled() | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Run migrations and seeds on VM | |
| uses: appleboy/ssh-action@v1 | |
| with: | |
| host: ${{ secrets.SSH_HOST }} | |
| username: ${{ secrets.SSH_USERNAME }} | |
| key: ${{ secrets.SSH_KEY }} | |
| passphrase: ${{ secrets.SSH_PASSPHRASE }} | |
| script: | | |
| set -euo pipefail | |
| # Load nvm if present, install Node 24 + pnpm if missing | |
| export NVM_DIR="$HOME/.nvm" | |
| if [ ! -s "$NVM_DIR/nvm.sh" ]; then | |
| curl -fsSL https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash | |
| fi | |
| . "$NVM_DIR/nvm.sh" | |
| nvm install 24 | |
| nvm use 24 | |
| corepack enable | |
| corepack prepare pnpm@9.0.0 --activate | |
| # Clone or update repo on VM | |
| DEPLOY_DIR="$HOME/exchange" | |
| REPO_DIR="$HOME/exchange-src" | |
| if [ ! -d "$REPO_DIR/.git" ]; then | |
| git clone https://github.com/${{ github.repository }}.git "$REPO_DIR" | |
| fi | |
| cd "$REPO_DIR" | |
| git fetch origin | |
| git checkout ${{ github.sha }} | |
| # Install workspace dependencies | |
| pnpm install --frozen-lockfile | |
| set -a | |
| . "$DEPLOY_DIR/.env" | |
| set +a | |
| : "${TIMESCALE_PASSWORD:?Set TIMESCALE_PASSWORD in $DEPLOY_DIR/.env}" | |
| MIGRATION_DATABASE_URL="${MIGRATION_DATABASE_URL:-postgresql://exchange:${TIMESCALE_PASSWORD}@localhost:5433/exchange}" | |
| cat > packages/database/.env <<EOF | |
| DATABASE_URL=$MIGRATION_DATABASE_URL | |
| DIRECT_URL=$MIGRATION_DATABASE_URL | |
| EOF | |
| cat > apps/db-processor/.env <<EOF | |
| DATABASE_URL=$MIGRATION_DATABASE_URL | |
| TIMESCALE_USER=exchange | |
| TIMESCALE_HOST=localhost | |
| TIMESCALE_DATABASE=exchange | |
| TIMESCALE_PASSWORD=$TIMESCALE_PASSWORD | |
| TIMESCALE_PORT=5433 | |
| EOF | |
| # Prisma generate + migrate | |
| pnpm --filter @repo/database exec prisma generate | |
| pnpm --filter @repo/database exec prisma migrate deploy | |
| # Seed Postgres markets (uses packages/database/.env) | |
| pnpm --filter @repo/database run seed | |
| # Seed TimescaleDB schema (uses apps/db-processor/.env) | |
| pnpm --filter db-processor run seed | |
| verify: | |
| needs: migrate | |
| if: always() && !failure() && !cancelled() | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Verify app health and prune images | |
| uses: appleboy/ssh-action@v1 | |
| with: | |
| host: ${{ secrets.SSH_HOST }} | |
| username: ${{ secrets.SSH_USERNAME }} | |
| key: ${{ secrets.SSH_KEY }} | |
| passphrase: ${{ secrets.SSH_PASSPHRASE }} | |
| script: | | |
| set -euo pipefail | |
| cd /home/${{ secrets.SSH_USERNAME }}/exchange | |
| # App containers may have been crash-looping until migrate finished. | |
| # Restart them once now that schema + seeds are in place. | |
| docker compose -f docker-compose-deploy.yml up -d --no-deps backend engine db-processor websocket market-maker | |
| sleep 10 | |
| curl -f --retry 5 --retry-delay 3 http://localhost:3001/health || exit 1 | |
| docker image prune -f |