Skip to content

deploy-to-home-server #69

deploy-to-home-server

deploy-to-home-server #69

name: deploy-to-home-server
on:
workflow_dispatch:
inputs:
fresh_start:
description: 'WARNING: This ERASES OPENCLAW WORKSPACE DATA - memory, skills, sessions, uploaded files. This action is IRREVERSIBLE. Obsidian vault volume is NOT removed.'
required: false
default: false
type: boolean
skip_git_sync:
description: 'Skip git sync on this deployment (use local workspace state only)'
required: false
default: false
type: boolean
jobs:
deploy:
runs-on: self-hosted
steps:
- name: Clean up work directory
run: sudo rm -rf ${{ github.workspace }}/credentials || true
- name: Checkout repository
uses: actions/checkout@v4
with:
submodules: recursive
- name: Debug User Info
run: |
echo "Current user: $(whoami)"
echo "User ID: $(id -u)"
echo "Group ID: $(id -g)"
echo "Groups: $(id -Gn)"
echo "Docker group members: $(getent group docker)"
echo "Docker socket permissions: $(ls -la /var/run/docker.sock 2>/dev/null || echo 'Docker socket not found')"
- name: Validate required repository variables
run: |
if [ -z "${{ vars.WORKSPACE_STATE_REPO }}" ]; then
echo "ERROR: Repository variable WORKSPACE_STATE_REPO is required"
exit 1
fi
GUI_BIND_IP="${{ vars.SYNCTHING_GUI_BIND_IP }}"
if [ -n "$GUI_BIND_IP" ]; then
if [ "$GUI_BIND_IP" = "0.0.0.0" ]; then
echo "ERROR: SYNCTHING_GUI_BIND_IP cannot be 0.0.0.0"
exit 1
fi
if ! echo "$GUI_BIND_IP" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$'; then
echo "ERROR: SYNCTHING_GUI_BIND_IP must be an IPv4 address when set"
exit 1
fi
fi
- name: Create .env file
run: |
umask 077
cat > .env << EOF
ZAI_API_KEY=${{ secrets.ZAI_API_KEY }}
TELEGRAM_BOT_TOKEN=${{ secrets.TELEGRAM_BOT_TOKEN }}
DEEPSEEK_API_KEY=${{ secrets.DEEPSEEK_API_KEY }}
OLLAMA_API_KEY=${{ secrets.OLLAMA_API_KEY }}
PRIMARY_TELEGRAM_ID=${{ secrets.PRIMARY_TELEGRAM_ID }}
GATEWAY_AUTH_PASSWORD=${{ secrets.GATEWAY_AUTH_PASSWORD }}
GOG_KEYRING_PASSWORD=${{ secrets.GOG_KEYRING_PASSWORD }}
TS_AUTHKEY=${{ secrets.TS_AUTHKEY }}
CONTROL_UI_ALLOWED_ORIGIN_1=${{ vars.CONTROL_UI_ALLOWED_ORIGIN_1 }}
CONTROL_UI_ALLOWED_ORIGIN_2=${{ vars.CONTROL_UI_ALLOWED_ORIGIN_2 }}
WORKSPACE_STATE_REPO=${{ vars.WORKSPACE_STATE_REPO }}
WORKSPACE_REPO_TOKEN=${{ secrets.WORKSPACE_REPO_TOKEN }}
WORKSPACE_SYNC_ON_START=${{ github.event.inputs.skip_git_sync == 'false' && 'true' || 'false' }}
WORKSPACE_SYNC_INTERVAL=60
WORKSPACE_MEMORY_DAYS=30
TZ=${{ vars.TZ }}
TAILSCALE_HOSTNAME=${{ vars.TAILSCALE_HOSTNAME }}
TS_EXTRA_ARGS=${{ vars.TS_EXTRA_ARGS }}
SYNCTHING_GUI_BIND_IP=${{ vars.SYNCTHING_GUI_BIND_IP }}
AUX_ML_ENABLED=${{ vars.AUX_ML_ENABLED == 'false' && 'false' || 'true' }}
COMPOSE_PROFILES=${{ vars.AUX_ML_ENABLED == 'false' && '' || 'aux-ml' }}
AUX_ML_URL=http://aux-ml:8091
AUX_ML_MEMORY_LIMIT=${{ vars.AUX_ML_MEMORY_LIMIT }}
AUX_ML_MEMORY_LIMIT_MB=${{ vars.AUX_ML_MEMORY_LIMIT_MB }}
AUX_ML_MAX_QUEUE=${{ vars.AUX_ML_MAX_QUEUE }}
AUX_ML_JOB_TIMEOUT_SECONDS=${{ vars.AUX_ML_JOB_TIMEOUT_SECONDS }}
AUX_ML_POLL_INTERVAL_SECONDS=${{ vars.AUX_ML_POLL_INTERVAL_SECONDS }}
AUX_ML_LLAMACPP_TIMEOUT_SECONDS=${{ vars.AUX_ML_LLAMACPP_TIMEOUT_SECONDS }}
AUX_ML_ALLOWED_INPUT_DIRS=${{ vars.AUX_ML_ALLOWED_INPUT_DIRS }}
AUX_ML_ENFORCE_MEMORY_LIMIT=${{ vars.AUX_ML_ENFORCE_MEMORY_LIMIT }}
AUX_ML_OCR_MAX_PAGES=${{ vars.AUX_ML_OCR_MAX_PAGES }}
EOF
chmod 600 .env
- name: Set compose profiles
run: |
AUX_ML_ENABLED="${{ vars.AUX_ML_ENABLED }}"
if [ -z "$AUX_ML_ENABLED" ]; then
AUX_ML_ENABLED="true"
fi
if [ "$AUX_ML_ENABLED" = "true" ]; then
echo "COMPOSE_PROFILES=aux-ml" >> "$GITHUB_ENV"
else
echo "COMPOSE_PROFILES=" >> "$GITHUB_ENV"
fi
- name: Validate aux-ml model source
if: vars.AUX_ML_ENABLED != 'false'
run: |
if [ -f "aux-ml/models/glm-ocr.gguf" ]; then
echo "Found local glm-ocr model in repository checkout"
else
echo "No local glm-ocr model found; compose default URL will be used"
fi
if [ -f "aux-ml/models/mmproj-glm-ocr.gguf" ]; then
echo "Found local mmproj model in repository checkout"
else
echo "No local mmproj model found; compose default URL will be used"
fi
- name: Create rclone config for backup container
run: |
if [ -n "${{ secrets.RCLONE_CONFIG_B64 }}" ]; then
COMPOSE_PROJECT=$(basename "$PWD")
RCLONE_VOLUME="${COMPOSE_PROJECT}_obsidian-rclone-config"
TMP_RCLONE_CONF=$(mktemp)
trap 'rm -f "$TMP_RCLONE_CONF"' EXIT
echo "${{ secrets.RCLONE_CONFIG_B64 }}" | base64 --decode > "$TMP_RCLONE_CONF"
chmod 600 "$TMP_RCLONE_CONF"
docker volume create "$RCLONE_VOLUME" >/dev/null
docker run --rm \
-v "$TMP_RCLONE_CONF:/tmp/rclone.conf:ro" \
-v "$RCLONE_VOLUME:/config/rclone" \
alpine:3.20 \
sh -c 'cp /tmp/rclone.conf /config/rclone/rclone.conf && chmod 600 /config/rclone/rclone.conf'
rm -f "$TMP_RCLONE_CONF"
trap - EXIT
echo "rclone config loaded into Docker volume"
else
echo "WARNING: RCLONE_CONFIG_B64 secret not set. Obsidian backups will fail until configured."
fi
- name: FRESH START - Remove workspace volume
if: github.event.inputs.fresh_start == 'true'
run: |
echo "=========================================="
echo "!!! WARNING: FRESH START REQUESTED !!!"
echo "=========================================="
echo "This will DELETE ALL agent data:"
echo " - All memory files (SOUL.md, MEMORY.md, AGENTS.md, etc.)"
echo " - All skills (including agent modifications)"
echo " - All session history and conversation data"
echo " - All uploaded files (PDFs, etc.)"
echo " - All workspace configuration"
echo " - Obsidian vault volume is NOT deleted"
echo "=========================================="
echo "Waiting 10 seconds before proceeding..."
echo "Press Cancel on this workflow to abort."
sleep 10
docker compose down || true
docker volume rm josemar-assistente_openclaw-workspace 2>/dev/null || true
echo "Workspace volume removed"
- name: Stop existing services
run: docker compose down
- name: Clean up old Docker images
run: |
docker image prune -a -f
docker builder prune -f
docker rmi $(docker images 'josemar-assistente*' -q) 2>/dev/null || true
- name: Build Docker image
run: docker compose build --no-cache
- name: Start services
run: docker compose up -d
- name: Verify tailscale sidecar login
run: |
echo "Checking tailscale sidecar status..."
if ! docker compose ps tailscale | grep -q "Up"; then
echo "ERROR: tailscale service is not running"
docker compose ps
exit 1
fi
TS_IP=""
for i in {1..30}; do
TS_IP="$(docker compose exec -T tailscale tailscale ip -4 2>/dev/null | sed -n '1p')"
if echo "$TS_IP" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$'; then
echo "Tailscale sidecar connected with IPv4: $TS_IP"
break
fi
echo "Waiting for tailscale login/connection... ($i/30)"
sleep 2
done
if ! echo "$TS_IP" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$'; then
echo "ERROR: tailscale sidecar is running but not logged in/connected"
echo "Set TS_AUTHKEY secret or log in manually with: docker compose exec -T tailscale tailscale up"
docker compose logs --tail=120 tailscale
exit 1
fi
- name: Wait for container to be healthy
run: |
echo "Waiting for container to be healthy..."
for i in {1..30}; do
if docker compose ps | grep -q "healthy"; then
echo "Container is healthy"
break
fi
echo " Waiting... ($i/30)"
sleep 2
done
- name: Verify deployment
run: docker ps | grep josemar-assistente
- name: Verify skill deployment
run: |
echo "Checking deployed skills..."
docker compose exec -T openclaw ls -la /root/.openclaw/skills/ 2>/dev/null || echo "No skills deployed yet"
- name: Cleanup sensitive files
if: always()
run: rm -f .env