Skip to content

Revert "Merge pull request #14 from azin-tech/claude-fix/issue-13" #4

Revert "Merge pull request #14 from azin-tech/claude-fix/issue-13"

Revert "Merge pull request #14 from azin-tech/claude-fix/issue-13" #4

name: Deploy to production VM
# Pushes to master → sync snake-game-production to origin/master
# and restart the Python server. Idempotent; skipped when only
# .github/** or markdown files change.
on:
push:
branches: [master]
paths-ignore:
- '.github/**'
- '**.md'
concurrency:
group: deploy-production
cancel-in-progress: true
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Install grpcurl
run: |
set -euo pipefail
curl -fsSL -o /tmp/grpcurl.tar.gz \
https://github.com/fullstorydev/grpcurl/releases/download/v1.9.1/grpcurl_1.9.1_linux_x86_64.tar.gz
tar -xzf /tmp/grpcurl.tar.gz -C /tmp grpcurl
sudo mv /tmp/grpcurl /usr/local/bin/grpcurl
- name: Exchange API key for JWT
env:
BOXD_API_KEY: ${{ secrets.BOXD_API_KEY }}
run: |
set -euo pipefail
jwt=$(curl -fsSL -X POST https://boxd-stg.sh/api/v1/auth/token \
-H 'Content-Type: application/json' \
-d "{\"api_key\":\"$BOXD_API_KEY\"}" | jq -r .token)
[[ -n "$jwt" && "$jwt" != "null" ]] || { echo "key exchange failed"; exit 1; }
echo "::add-mask::$jwt"
echo "BOXD_JWT=$jwt" >> "$GITHUB_ENV"
- name: Resolve production VM id
run: |
set -euo pipefail
vm_id=$(grpcurl -plaintext -H "authorization: Bearer $BOXD_JWT" -d '{}' \
boxd-stg.sh:9443 boxd.api.v1.BoxdApi/ListVms \
| jq -r '.vms[] | select(.name=="snake-game-production") | .vmId')
[[ -n "$vm_id" ]] || { echo "snake-game-production VM not found"; exit 1; }
echo "PROD_VM_ID=$vm_id" >> "$GITHUB_ENV"
- name: Pull latest master + restart server
run: |
set -euo pipefail
# One-line the command so printf %q doesn't emit ANSI-C quoting
# that would be destroyed by the JSON round-trip through grpcurl.
# Kill the server by port (fuser), not by name (pkill -f), because
# the deploy script itself contains the literal "python3 server.py"
# and pkill -f would match its own bash and commit suicide.
inner='cd /home/boxd/first-boot && git fetch origin && git reset --hard origin/master && (fuser -k 3000/tcp 2>/dev/null || true) && sleep 1 && (setsid nohup python3 server.py >/tmp/server.log 2>&1 </dev/null &) && sleep 2 && curl -sf -o /dev/null http://localhost:3000/ && echo "deployed $(git rev-parse --short HEAD)"'
wrapped="bash -lc $(printf %q "$inner")"
msg=$(jq -nc --arg vm "$PROD_VM_ID" --arg cmd "$wrapped" '{vm_id:$vm,command:$cmd}')
for attempt in 1 2 3 4 5; do
if out=$(printf '%s' "$msg" | grpcurl -plaintext -emit-defaults -max-time 120 \
-H "authorization: Bearer $BOXD_JWT" -d @ \
boxd-stg.sh:9443 boxd.api.v1.BoxdApi/Exec 2>&1); then
echo "$out" | jq -s -r 'map(.data // "") | join("")' | base64 -d 2>/dev/null || true
rc=$(echo "$out" | jq -s 'last.exitCode // 0')
exit "$rc"
fi
if echo "$out" | grep -qE "connection to VM|Unavailable|No route to host"; then
echo "transient VM-unreachable (attempt $attempt), retrying in $((attempt*3))s..."
sleep $((attempt*3))
continue
fi
echo "exec failed:"
echo "$out"
exit 1
done
echo "exec failed after retries"
exit 1
- name: Verify production URL
run: |
set -euo pipefail
for i in 1 2 3 4 5; do
code=$(curl -sS -o /dev/null -w '%{http_code}' https://snake-game-production.boxd-stg.sh/ --max-time 10 || true)
echo "try $i: $code"
[[ "$code" == "200" ]] && exit 0
sleep 3
done
echo "production URL did not return 200"
exit 1