feat: add GitHub Actions workflow for automatic Hostinger VPS deployment

Deploys PicoClaw to Hostinger VPS via SSH on every push to main.
Uses sshpass + rsync for file sync and Docker Compose for container management.
Includes health check verification after deployment.

Required GitHub secrets: HOSTINGER_HOST, HOSTINGER_SSH_PASSWORD,
HOSTINGER_SSH_USER (optional), HOSTINGER_SSH_PORT (optional).

https://claude.ai/code/session_019vXaqxGmkdCjM8m3jp6rYj
This commit is contained in:
Claude 2026-02-17 16:46:55 +00:00
parent f3e727a3d0
commit a0e6d816a8
No known key found for this signature in database

141
.github/workflows/deploy-hostinger.yml vendored Normal file
View file

@ -0,0 +1,141 @@
name: Deploy to Hostinger VPS
on:
push:
branches: ["main"]
workflow_dispatch:
env:
REMOTE_DIR: /opt/picoclaw
DEPLOY_METHOD: docker
jobs:
deploy:
name: Deploy to Hostinger
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install sshpass
run: sudo apt-get update -qq && sudo apt-get install -y -qq sshpass rsync
- name: Verify SSH connection
env:
SSHPASS: ${{ secrets.HOSTINGER_SSH_PASSWORD }}
run: |
sshpass -e ssh \
-o StrictHostKeyChecking=accept-new \
-o ConnectTimeout=15 \
-p ${{ secrets.HOSTINGER_SSH_PORT || '22' }} \
${{ secrets.HOSTINGER_SSH_USER || 'root' }}@${{ secrets.HOSTINGER_HOST }} \
"echo 'SSH connection OK'"
- name: Sync project files to server
env:
SSHPASS: ${{ secrets.HOSTINGER_SSH_PASSWORD }}
run: |
SSH_PORT="${{ secrets.HOSTINGER_SSH_PORT }}"
SSH_PORT="${SSH_PORT:-22}"
SSH_USER="${{ secrets.HOSTINGER_SSH_USER }}"
SSH_USER="${SSH_USER:-root}"
rsync -az --delete \
-e "sshpass -e ssh -o StrictHostKeyChecking=accept-new -o ConnectTimeout=15 -p ${SSH_PORT}" \
--exclude '.git' \
--exclude 'build/' \
--exclude '.env' \
--exclude 'node_modules/' \
--exclude '.deploy.env' \
./ "${SSH_USER}@${{ secrets.HOSTINGER_HOST }}:${REMOTE_DIR}/src/"
- name: Copy Docker Compose production config
env:
SSHPASS: ${{ secrets.HOSTINGER_SSH_PASSWORD }}
run: |
SSH_PORT="${{ secrets.HOSTINGER_SSH_PORT }}"
SSH_PORT="${SSH_PORT:-22}"
SSH_USER="${{ secrets.HOSTINGER_SSH_USER }}"
SSH_USER="${SSH_USER:-root}"
sshpass -e scp \
-o StrictHostKeyChecking=accept-new \
-o ConnectTimeout=15 \
-P "${SSH_PORT}" \
deploy/hostinger/docker-compose.production.yml \
"${SSH_USER}@${{ secrets.HOSTINGER_HOST }}:${REMOTE_DIR}/docker-compose.yml"
- name: Build and restart Docker container
env:
SSHPASS: ${{ secrets.HOSTINGER_SSH_PASSWORD }}
run: |
SSH_PORT="${{ secrets.HOSTINGER_SSH_PORT }}"
SSH_PORT="${SSH_PORT:-22}"
SSH_USER="${{ secrets.HOSTINGER_SSH_USER }}"
SSH_USER="${SSH_USER:-root}"
sshpass -e ssh \
-o StrictHostKeyChecking=accept-new \
-o ConnectTimeout=15 \
-p "${SSH_PORT}" \
"${SSH_USER}@${{ secrets.HOSTINGER_HOST }}" <<'EOF'
set -e
cd /opt/picoclaw
echo "Building Docker image..."
docker compose build picoclaw-gateway
echo "Stopping existing container..."
docker compose down --timeout 30 2>/dev/null || true
echo "Starting PicoClaw gateway..."
docker compose up -d picoclaw-gateway
echo "Waiting for container to start..."
sleep 5
if docker compose ps picoclaw-gateway 2>/dev/null | grep -qE "Up|running"; then
echo "Container is running!"
docker compose ps
else
echo "Container status:"
docker compose ps
echo ""
echo "Container logs:"
docker compose logs --tail=30 picoclaw-gateway
exit 1
fi
docker image prune -f >/dev/null 2>&1
echo "Docker deploy complete"
EOF
- name: Health check
env:
SSHPASS: ${{ secrets.HOSTINGER_SSH_PASSWORD }}
run: |
SSH_PORT="${{ secrets.HOSTINGER_SSH_PORT }}"
SSH_PORT="${SSH_PORT:-22}"
SSH_USER="${{ secrets.HOSTINGER_SSH_USER }}"
SSH_USER="${SSH_USER:-root}"
HEALTH_OK=false
for i in 1 2 3 4 5; do
sleep 3
if sshpass -e ssh \
-o StrictHostKeyChecking=accept-new \
-o ConnectTimeout=15 \
-p "${SSH_PORT}" \
"${SSH_USER}@${{ secrets.HOSTINGER_HOST }}" \
"curl -sf http://localhost:18790/health" 2>/dev/null; then
HEALTH_OK=true
echo ""
echo "Health check PASSED!"
break
fi
echo "Waiting for gateway to start... (attempt ${i}/5)"
done
if [ "${HEALTH_OK}" = false ]; then
echo "::warning::Health check did not pass after 5 attempts. The service may still be starting up."
fi