Deploy to Droplet #589
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
| # Deploy to DigitalOcean Droplet | |
| # Deploys the application to a DigitalOcean Droplet via rsync + SSH | |
| # Requires UDP ports open for BitTorrent DHT to work | |
| # | |
| # This workflow runs AFTER the CI workflow completes successfully | |
| # to avoid duplicate test runs | |
| name: Deploy to Droplet | |
| on: | |
| # Run after CI workflow completes successfully | |
| workflow_run: | |
| workflows: ["CI Pipeline"] | |
| types: [completed] | |
| branches: [main, master] | |
| workflow_dispatch: # Allow manual trigger | |
| concurrency: | |
| group: deploy-droplet | |
| cancel-in-progress: true | |
| env: | |
| # Deploy path: /home/ubuntu/www/:domain/:repo | |
| DEPLOY_PATH: /home/ubuntu/www/bittorrented.com/media-streamer | |
| SERVICE_NAME: bittorrented | |
| jobs: | |
| # Deploy to Droplet (only if CI passed) | |
| deploy: | |
| name: Deploy to Droplet | |
| runs-on: ubuntu-latest | |
| # Only deploy if CI workflow succeeded (or manual trigger) | |
| if: | | |
| github.event_name == 'workflow_dispatch' || | |
| (github.event.workflow_run.conclusion == 'success' && | |
| (github.event.workflow_run.head_branch == 'main' || github.event.workflow_run.head_branch == 'master')) | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Write .env file | |
| env: | |
| ENV_FILE_CONTENT: ${{ secrets.ENV_FILE }} | |
| run: | | |
| printf '%s\n' "$ENV_FILE_CONTENT" > .env | |
| - name: Setup SSH key | |
| run: | | |
| mkdir -p ~/.ssh | |
| echo "${{ secrets.DROPLET_SSH_KEY }}" > ~/.ssh/deploy_key | |
| chmod 600 ~/.ssh/deploy_key | |
| # Add host to known_hosts (with retry for transient failures) | |
| for i in 1 2 3; do | |
| ssh-keyscan -p ${{ secrets.DROPLET_PORT || 22 }} -H ${{ secrets.DROPLET_HOST }} >> ~/.ssh/known_hosts 2>/dev/null && break | |
| sleep 2 | |
| done | |
| - name: Sync files to Droplet (with retry) | |
| uses: nick-fields/retry@v3 | |
| with: | |
| timeout_minutes: 15 | |
| max_attempts: 5 | |
| retry_wait_seconds: 5 | |
| command: | | |
| SSH_OPTS="-i ~/.ssh/deploy_key -p ${{ secrets.DROPLET_PORT || 22 }} -o StrictHostKeyChecking=no -o ConnectTimeout=60 -o ServerAliveInterval=30 -o ServerAliveCountMax=5" | |
| # Create deploy directory if it doesn't exist | |
| ssh $SSH_OPTS ${{ secrets.DROPLET_USER }}@${{ secrets.DROPLET_HOST }} "mkdir -p ${{ env.DEPLOY_PATH }}" | |
| # Sync files | |
| rsync -azv --partial --progress --delete \ | |
| --exclude='.git' \ | |
| --exclude='node_modules' \ | |
| --exclude='.next' \ | |
| --exclude='.env.local' \ | |
| --exclude='*.log' \ | |
| -e "ssh $SSH_OPTS" \ | |
| ./ ${{ secrets.DROPLET_USER }}@${{ secrets.DROPLET_HOST }}:${{ env.DEPLOY_PATH }}/ | |
| - name: Start build on Droplet (background) | |
| run: | | |
| SSH_OPTS="-i ~/.ssh/deploy_key -p ${{ secrets.DROPLET_PORT || 22 }} -o StrictHostKeyChecking=no -o ConnectTimeout=60" | |
| # Create the build script on the server | |
| ssh $SSH_OPTS ${{ secrets.DROPLET_USER }}@${{ secrets.DROPLET_HOST }} << 'SETUP_SCRIPT' | |
| cat > /tmp/deploy-build.sh << 'BUILD_SCRIPT' | |
| #!/bin/bash | |
| set -e | |
| LOG_FILE="/tmp/deploy-build.log" | |
| STATUS_FILE="/tmp/deploy-build.status" | |
| # Clear previous status | |
| echo "running" > "$STATUS_FILE" | |
| { | |
| cd /home/ubuntu/www/bittorrented.com/media-streamer | |
| # Run idempotent setup script | |
| echo "=== Running setup script ===" | |
| bash scripts/setup-server.sh | |
| # Source profile to get pnpm in PATH | |
| export PNPM_HOME="$HOME/.local/share/pnpm" | |
| export PATH="$PNPM_HOME:$HOME/.pnpm:$HOME/.npm-global/bin:/usr/local/bin:$PATH" | |
| [ -f ~/.bashrc ] && source ~/.bashrc 2>/dev/null || true | |
| [ -f ~/.profile ] && source ~/.profile 2>/dev/null || true | |
| echo "" | |
| echo "=== Environment ===" | |
| echo "PATH: $PATH" | |
| echo "Node: $(node --version 2>/dev/null || echo 'not found')" | |
| echo "pnpm: $(which pnpm 2>/dev/null || echo 'not found')" | |
| echo "" | |
| echo "=== System resources ===" | |
| free -h | |
| df -h / | |
| echo "" | |
| echo "=== Cleaning build cache ===" | |
| # Acquire exclusive lock — wait for any previous deploy to finish | |
| exec 200>/tmp/deploy-build.lock | |
| flock -w 300 200 || { echo "✗ Lock timeout after 300s"; echo "failed" > "$STATUS_FILE"; exit 1; } | |
| # Kill any lingering next build processes | |
| pkill -f "next build" 2>/dev/null || true | |
| sleep 1 | |
| rm -rf .next 2>/dev/null || true | |
| echo "✓ Build cache cleaned" | |
| echo "" | |
| echo "=== Installing dependencies ===" | |
| pnpm install --frozen-lockfile | |
| echo "✓ Dependencies installed" | |
| echo "" | |
| echo "=== Building application ===" | |
| nice -n 10 pnpm build || { echo "✗ Build FAILED"; echo "failed" > "$STATUS_FILE"; exit 1; } | |
| echo "✓ Build complete" | |
| echo "" | |
| # Verify build output exists before restarting | |
| if [ ! -f .next/standalone/server.js ]; then | |
| echo "✗ Build output missing (.next/standalone/server.js not found)" | |
| echo "failed" > "$STATUS_FILE" | |
| exit 1 | |
| fi | |
| echo "=== Restarting services ===" | |
| sudo systemctl restart bittorrented || echo "Warning: Could not restart main service" | |
| echo "✓ Main service restart attempted" | |
| sudo systemctl restart bittorrented-iptv-worker || echo "Warning: Could not restart IPTV worker" | |
| echo "✓ IPTV worker restart attempted" | |
| sleep 3 | |
| echo "" | |
| echo "=== Verifying deployment ===" | |
| systemctl is-active bittorrented || echo "Main service status unknown" | |
| systemctl is-active bittorrented-iptv-worker || echo "IPTV worker status unknown" | |
| echo "" | |
| echo "=== Deployment complete! ===" | |
| echo "success" > "$STATUS_FILE" | |
| } >> "$LOG_FILE" 2>&1 || { | |
| echo "failed" > "$STATUS_FILE" | |
| exit 1 | |
| } | |
| BUILD_SCRIPT | |
| chmod +x /tmp/deploy-build.sh | |
| SETUP_SCRIPT | |
| # Start the build in background using nohup | |
| ssh $SSH_OPTS ${{ secrets.DROPLET_USER }}@${{ secrets.DROPLET_HOST }} \ | |
| "nohup /tmp/deploy-build.sh > /dev/null 2>&1 &" | |
| echo "Build started in background on droplet" | |
| - name: Wait for build to complete | |
| run: | | |
| SSH_OPTS="-i ~/.ssh/deploy_key -p ${{ secrets.DROPLET_PORT || 22 }} -o StrictHostKeyChecking=no -o ConnectTimeout=30" | |
| echo "Waiting for build to complete..." | |
| MAX_WAIT=600 # 10 minutes | |
| ELAPSED=0 | |
| POLL_INTERVAL=10 | |
| while [ $ELAPSED -lt $MAX_WAIT ]; do | |
| # Check build status | |
| STATUS=$(ssh $SSH_OPTS ${{ secrets.DROPLET_USER }}@${{ secrets.DROPLET_HOST }} \ | |
| "cat /tmp/deploy-build.status 2>/dev/null || echo 'unknown'" || echo "ssh_error") | |
| if [ "$STATUS" = "success" ]; then | |
| echo "✓ Build completed successfully!" | |
| # Show the log | |
| ssh $SSH_OPTS ${{ secrets.DROPLET_USER }}@${{ secrets.DROPLET_HOST }} \ | |
| "tail -50 /tmp/deploy-build.log" || true | |
| exit 0 | |
| elif [ "$STATUS" = "failed" ]; then | |
| echo "✗ Build failed!" | |
| ssh $SSH_OPTS ${{ secrets.DROPLET_USER }}@${{ secrets.DROPLET_HOST }} \ | |
| "cat /tmp/deploy-build.log" || true | |
| exit 1 | |
| elif [ "$STATUS" = "ssh_error" ]; then | |
| echo "SSH connection failed, retrying in ${POLL_INTERVAL}s..." | |
| else | |
| echo "Build still running... (${ELAPSED}s elapsed)" | |
| fi | |
| sleep $POLL_INTERVAL | |
| ELAPSED=$((ELAPSED + POLL_INTERVAL)) | |
| done | |
| echo "✗ Build timed out after ${MAX_WAIT}s" | |
| ssh $SSH_OPTS ${{ secrets.DROPLET_USER }}@${{ secrets.DROPLET_HOST }} \ | |
| "cat /tmp/deploy-build.log" || true | |
| exit 1 | |
| - name: Cleanup SSH key | |
| if: always() | |
| run: rm -f ~/.ssh/deploy_key |