Deploy to VPS #2657
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: Deploy to VPS | |
| on: | |
| workflow_run: | |
| workflows: ["Publish to Docker Hub"] | |
| types: [completed] | |
| workflow_dispatch: | |
| permissions: | |
| contents: read | |
| jobs: | |
| deploy: | |
| if: >- | |
| (github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success') | |
| && vars.DEPLOY_ENABLED == 'true' | |
| name: Deploy OmniRoute to VPS | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Check VPS SSH reachability from runner | |
| id: reach | |
| env: | |
| # Pass the host via env (never interpolate a secret straight into the | |
| # script body) so /dev/tcp gets a shell variable, not inlined text. | |
| VPS_HOST: ${{ secrets.VPS_HOST }} | |
| run: | | |
| set -uo pipefail | |
| # A GitHub-hosted runner can only deploy when it can actually open a TCP | |
| # connection to the VPS SSH port. The Local VPS lives on a private LAN and | |
| # the Akamai host firewalls :22 to known IPs, so the runner is routinely | |
| # unable to reach it (`dial tcp ***:22: i/o timeout`). Treat "unreachable | |
| # from the runner" as a SKIP — the real deploys are run manually from an | |
| # allowed network via the deploy-vps-local / deploy-vps-akamai skills — so | |
| # an unreachable host no longer red-fails every release/push pipeline. | |
| # When the host IS reachable, the deploy step below still runs in full and | |
| # its health gate surfaces any genuine deploy failure. | |
| if timeout 15 bash -c 'exec 3<>"/dev/tcp/${VPS_HOST}/22"' 2>/dev/null; then | |
| echo "reachable=true" >> "$GITHUB_OUTPUT" | |
| echo "✅ VPS_HOST:22 reachable from the runner — proceeding with deploy." | |
| else | |
| echo "reachable=false" >> "$GITHUB_OUTPUT" | |
| echo "::warning title=Auto-deploy skipped::VPS_HOST:22 is not reachable from this GitHub runner (private LAN / firewalled). Deploy manually with the deploy-vps-local or deploy-vps-akamai skill." | |
| fi | |
| - name: Deploy via SSH | |
| if: steps.reach.outputs.reachable == 'true' | |
| uses: appleboy/ssh-action@v1 | |
| with: | |
| host: ${{ secrets.VPS_HOST }} | |
| username: ${{ secrets.VPS_USER }} | |
| key: ${{ secrets.VPS_SSH_KEY }} | |
| port: 22 | |
| timeout: 60s | |
| command_timeout: 15m | |
| script: | | |
| set -euo pipefail | |
| echo "=== Updating OmniRoute ===" | |
| npm install -g omniroute@latest | |
| INSTALLED_VERSION=$(omniroute --version 2>/dev/null | tr -d '[:space:]' || echo "unknown") | |
| echo "Installed CLI version: $INSTALLED_VERSION" | |
| # Recreate the PM2 process instead of `pm2 restart`. A bare restart | |
| # re-runs whatever script path was saved earlier; after the build-output | |
| # reorg (app/ -> dist/, .next -> .build/next) a process pinned to the old | |
| # app/server-ws.mjs path can no longer start, and the node process dies | |
| # while PM2 still reports "online" — so the box never binds :20128. | |
| # Always launch via the `omniroute` bin so .env is loaded and the dist/ | |
| # layout is resolved correctly. | |
| echo "=== (Re)creating PM2 process via bin ===" | |
| pm2 delete omniroute 2>/dev/null || true | |
| pm2 start omniroute --name omniroute -- --port 20128 | |
| pm2 save | |
| # Health gate: fail the deploy unless the box actually reports healthy. | |
| # Poll /api/monitoring/health for "status":"healthy" (a deeper signal than | |
| # a static page 200 — it confirms the app booted, not just that a port is | |
| # bound). Boot can take a while after a native-module/build-layout change, | |
| # so poll up to ~3min before giving up. | |
| echo "=== Health Check (gates the deploy) ===" | |
| ok=0 | |
| for i in $(seq 1 36); do | |
| BODY=$(curl -sf -m 5 http://localhost:20128/api/monitoring/health 2>/dev/null || true) | |
| if printf '%s' "$BODY" | grep -q '"status":"healthy"'; then | |
| ok=1 | |
| echo "✅ /api/monitoring/health -> healthy (attempt $i) — version $INSTALLED_VERSION" | |
| break | |
| fi | |
| echo "… not healthy yet (attempt $i/36), retrying in 5s" | |
| sleep 5 | |
| done | |
| if [ "$ok" != "1" ]; then | |
| echo "❌ Health check failed — /api/monitoring/health never reported healthy after ~3min" | |
| echo "--- recent PM2 logs ---" | |
| pm2 logs omniroute --lines 40 --nostream || true | |
| exit 1 | |
| fi | |
| echo "=== Deploy complete ===" |