Replace fragile grep-based extraction that matched volume names (nats_data, tools_data) and 'null' as pseudo-services.
240 lines
9.7 KiB
YAML
240 lines
9.7 KiB
YAML
name: Deploy Docker to VPS
|
||
|
||
on:
|
||
workflow_run:
|
||
workflows: ['Build and Push Docker Images']
|
||
types:
|
||
- completed
|
||
branches:
|
||
- main
|
||
push:
|
||
branches:
|
||
- main
|
||
paths:
|
||
- 'infra/**'
|
||
- '.github/workflows/deploy-docker.yml'
|
||
- '.github/workflows/docker-build-push.yml'
|
||
workflow_dispatch:
|
||
|
||
# Prevent multiple deployments from running simultaneously
|
||
concurrency:
|
||
group: deploy-vps
|
||
cancel-in-progress: false
|
||
|
||
permissions:
|
||
contents: read
|
||
packages: read
|
||
|
||
jobs:
|
||
deploy:
|
||
runs-on: ubuntu-latest
|
||
timeout-minutes: 30
|
||
if: github.event_name == 'workflow_dispatch' || github.event_name == 'push' || github.event.workflow_run.conclusion == 'success'
|
||
steps:
|
||
- name: Checkout repository
|
||
uses: actions/checkout@v7
|
||
with:
|
||
fetch-depth: 1
|
||
submodules: false
|
||
|
||
- name: Deploy to VPS
|
||
env:
|
||
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
|
||
VPS_HOST: ${{ secrets.VPS_HOST }}
|
||
VPS_USER: ${{ secrets.VPS_USER }}
|
||
VPS_TARGET_DIR: ${{ secrets.VPS_TARGET_DIR }}
|
||
ENV_FILE_PRODUCTION: ${{ secrets.ENV_FILE_PRODUCTION }}
|
||
GHCR_USERNAME: ${{ github.actor }}
|
||
GHCR_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||
run: |
|
||
set -euo pipefail
|
||
|
||
echo "Deploy event: ${{ github.event_name }}"
|
||
echo "Deploy ref: ${{ github.ref }}"
|
||
echo "Deploy sha: ${{ github.sha }}"
|
||
|
||
if [ -z "${SSH_PRIVATE_KEY:-}" ] || [ -z "${VPS_HOST:-}" ] || [ -z "${VPS_USER:-}" ] || [ -z "${VPS_TARGET_DIR:-}" ]; then
|
||
echo "❌ Deployment secrets are not fully configured. Please set SSH_PRIVATE_KEY, VPS_HOST, VPS_USER, and VPS_TARGET_DIR."
|
||
exit 1
|
||
fi
|
||
|
||
mkdir -p ~/.ssh
|
||
echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
|
||
chmod 600 ~/.ssh/id_rsa
|
||
ssh-keyscan -H -t ed25519,rsa "$VPS_HOST" >> ~/.ssh/known_hosts
|
||
|
||
# Use SSH multiplexing for faster subsequent commands
|
||
SSH_OPTS=(-o ControlMaster=auto -o ControlPath=/tmp/ssh-%r@%h:%p -o ControlPersist=600 -o StrictHostKeyChecking=yes)
|
||
|
||
ssh "${SSH_OPTS[@]}" "$VPS_USER@$VPS_HOST" "mkdir -p $VPS_TARGET_DIR && mkdir -p $VPS_TARGET_DIR/infra/compose"
|
||
echo "$ENV_FILE_PRODUCTION" > .env.prod
|
||
scp "${SSH_OPTS[@]}" .env.prod "$VPS_USER@$VPS_HOST:$VPS_TARGET_DIR/.env"
|
||
|
||
echo "🔐 Logging in to GitHub Container Registry..."
|
||
printf '%s' "$GHCR_TOKEN" | ssh "${SSH_OPTS[@]}" "$VPS_USER@$VPS_HOST" "docker login ghcr.io -u '$GHCR_USERNAME' --password-stdin"
|
||
|
||
ssh "${SSH_OPTS[@]}" "$VPS_USER@$VPS_HOST" "export VPS_TARGET_DIR=$VPS_TARGET_DIR; bash -s" <<'EOF'
|
||
set -euo pipefail
|
||
cd "$VPS_TARGET_DIR"
|
||
|
||
# Ensure shared network exists
|
||
docker network inspect app-shared-net >/dev/null 2>&1 || docker network create app-shared-net
|
||
|
||
echo "🔄 Synchronizing repository..."
|
||
if [ ! -d ".git" ]; then
|
||
echo "Initializing git repository..."
|
||
git init
|
||
git remote add origin https://github.com/asepharyana/asepharyana-hub.git
|
||
fi
|
||
git fetch origin main --depth=1 || true
|
||
|
||
# Detect changed files before resetting
|
||
ALL_COMPOSE_FILES="infra/compose/traefik.yml infra/compose/shared.yml infra/compose/scraper.yml infra/compose/hub.yml infra/compose/tools.yml infra/compose/llm-api.yml infra/compose/nats.yml infra/compose/dapr.yml infra/compose/observability.yml"
|
||
TRAEFIK_DYNAMIC_DIR="infra/traefik/dynamic"
|
||
|
||
if git rev-parse HEAD >/dev/null 2>&1; then
|
||
BEFORE_REV=$(git rev-parse HEAD)
|
||
git reset --hard FETCH_HEAD
|
||
AFTER_REV=$(git rev-parse HEAD)
|
||
|
||
if [ "$BEFORE_REV" = "$AFTER_REV" ]; then
|
||
echo "ℹ️ No new commits detected. Using full file list for safety."
|
||
TARGET_COMPOSE=""
|
||
else
|
||
CHANGED=$(git diff --name-only "$BEFORE_REV" "$AFTER_REV" || true)
|
||
echo "📄 Changed files:"
|
||
echo "$CHANGED"
|
||
|
||
# Detect compose stack changes
|
||
CHANGED_COMPOSE=$(echo "$CHANGED" | grep '^infra/compose/.*\.yml$' || true)
|
||
TARGET_COMPOSE=""
|
||
for f in $CHANGED_COMPOSE; do
|
||
case " $ALL_COMPOSE_FILES " in
|
||
*" $f "*) TARGET_COMPOSE="$TARGET_COMPOSE $f" ;;
|
||
esac
|
||
done
|
||
TARGET_COMPOSE=$(printf '%s' "$TARGET_COMPOSE" | xargs || true)
|
||
if [ -n "$TARGET_COMPOSE" ]; then
|
||
echo "🎯 Detected compose stack changes in: $TARGET_COMPOSE"
|
||
else
|
||
echo "ℹ️ No stack compose files changed."
|
||
fi
|
||
|
||
# Detect Traefik dynamic config changes
|
||
CHANGED_TRAEFIK=$(echo "$CHANGED" | grep "^$TRAEFIK_DYNAMIC_DIR/" || true)
|
||
if [ -n "$CHANGED_TRAEFIK" ]; then
|
||
echo "🎯 Detected Traefik dynamic config changes:"
|
||
echo "$CHANGED_TRAEFIK"
|
||
RELOAD_TRAEFIK="true"
|
||
else
|
||
echo "ℹ️ No Traefik dynamic config changes."
|
||
fi
|
||
|
||
# Detect infra file changes (Dockerfiles, config, traefik static)
|
||
CHANGED_INFRA=$(echo "$CHANGED" | grep '^infra/' | grep -v '^infra/compose/' || true)
|
||
if [ -n "$CHANGED_INFRA" ]; then
|
||
echo "📦 Detected other infra file changes:"
|
||
echo "$CHANGED_INFRA"
|
||
fi
|
||
fi
|
||
else
|
||
git reset --hard FETCH_HEAD
|
||
TARGET_COMPOSE=""
|
||
fi
|
||
|
||
if command -v "docker" >/dev/null 2>&1 && docker compose version >/dev/null 2>&1; then
|
||
COMPOSE_CMD="docker compose"
|
||
elif command -v docker-compose >/dev/null 2>&1; then
|
||
COMPOSE_CMD="docker-compose"
|
||
else
|
||
echo "❌ docker compose is not installed on the remote host."
|
||
exit 1
|
||
fi
|
||
|
||
# Always include ALL compose files for dependency resolution
|
||
COMPOSE_ARGS=""
|
||
for f in $ALL_COMPOSE_FILES; do
|
||
if [ -f "$f" ]; then
|
||
COMPOSE_ARGS="$COMPOSE_ARGS -f $f"
|
||
fi
|
||
done
|
||
|
||
if [ -n "$TARGET_COMPOSE" ]; then
|
||
# Extract service names from target compose file(s) for selective up
|
||
TARGET_SERVICES=""
|
||
for f in $TARGET_COMPOSE; do
|
||
if [ -f "$f" ]; then
|
||
svcs=$($COMPOSE_CMD -f "$f" config --services 2>/dev/null | tr '\n' ' ' | xargs)
|
||
TARGET_SERVICES="$TARGET_SERVICES $svcs"
|
||
fi
|
||
done
|
||
TARGET_SERVICES=$(echo "$TARGET_SERVICES" | xargs) # trim whitespace
|
||
echo "🎯 Selective update for services: $TARGET_SERVICES"
|
||
else
|
||
echo "🚀 Performing full deployment of all services..."
|
||
TARGET_SERVICES=""
|
||
fi
|
||
|
||
echo "📥 Pulling images for target services..."
|
||
export DOCKER_CLI_EXPERIMENTAL=enabled
|
||
PULL_SUCCESS=false
|
||
# Retry pull up to 3 times to handle transient Docker attestation lease errors
|
||
for attempt in 1 2 3; do
|
||
echo "Pull attempt $attempt/3..."
|
||
if $COMPOSE_CMD $COMPOSE_ARGS --env-file .env pull $TARGET_SERVICES; then
|
||
echo "✅ Pull succeeded on attempt $attempt"
|
||
PULL_SUCCESS=true
|
||
break
|
||
else
|
||
echo "⚠️ Pull attempt $attempt failed. Retrying in 5s..."
|
||
sleep 5
|
||
fi
|
||
done
|
||
if [ "$PULL_SUCCESS" != "true" ]; then
|
||
echo "❌ Failed to pull images after 3 attempts."
|
||
exit 1
|
||
fi
|
||
|
||
echo "🧹 Clearing Git locks..."
|
||
rm -f .git/shallow.lock || true
|
||
|
||
echo "🧹 Removing stale target containers by container_name..."
|
||
# Extract all explicitly defined container_names from compose files and remove them to prevent conflicts
|
||
if [ -n "$TARGET_COMPOSE" ]; then
|
||
for f in $TARGET_COMPOSE; do
|
||
if [ -f "$f" ]; then
|
||
grep "container_name:" "$f" | awk '{print $2}' | while read -r cname; do
|
||
docker rm -f "$cname" >/dev/null 2>&1 || true
|
||
done
|
||
fi
|
||
done
|
||
else
|
||
for f in $ALL_COMPOSE_FILES; do
|
||
if [ -f "$f" ]; then
|
||
grep "container_name:" "$f" | awk '{print $2}' | while read -r cname; do
|
||
docker rm -f "$cname" >/dev/null 2>&1 || true
|
||
done
|
||
fi
|
||
done
|
||
fi
|
||
|
||
echo "🆙 Starting services..."
|
||
echo "🔍 Debug: Current docker containers:"
|
||
docker ps -a
|
||
if [ -n "$TARGET_SERVICES" ]; then
|
||
$COMPOSE_CMD $COMPOSE_ARGS --env-file .env up -d $TARGET_SERVICES
|
||
else
|
||
$COMPOSE_CMD $COMPOSE_ARGS --env-file .env up -d --remove-orphans
|
||
fi
|
||
|
||
# ── Traefik reload ──
|
||
if [ "${RELOAD_TRAEFIK:-false}" = "true" ]; then
|
||
echo "🔄 Traefik dynamic config changed — reloading Traefik..."
|
||
# Traefik watches the dynamic config dir (providers.file.watch=true),
|
||
# but send SIGHUP as insurance
|
||
docker kill --signal HUP traefik 2>/dev/null || docker exec traefik kill -HUP 1 2>/dev/null || true
|
||
echo "✅ Traefik reload signal sent"
|
||
fi
|
||
EOF
|
||
|