#!/usr/bin/env bash # Pull and deploy on server B with configuration injected over SSH by Gitea. # Returns nonzero on configuration/pull/migration/health failure. Never removes data. set -Eeuo pipefail umask 077 cd "$(dirname "${BASH_SOURCE[0]}")/.." # Fail before touching the host; never read a checkout's local .env file. for key in IMAGE_PREFIX DEPLOY_TAG REGISTRY_USERNAME REGISTRY_PASSWORD WQ_EMAIL WQ_PASSWORD DATABASE_URL ADMIN_PASSWORD ENCRYPTION_KEY PUBLIC_ORIGIN; do if [[ -z "${!key:-}" ]]; then echo "Missing required Gitea configuration: $key" >&2 exit 1 fi done compose=(docker compose --env-file /dev/null -p wq-alpha-production -f compose.production.yaml) lock_id="" auth_dir="" cleanup() { local rc=$? "${compose[@]}" --profile jobs ps -a || true # Remove only the lock acquired by this process, never another deployment's lock. if [[ -n "$lock_id" ]]; then docker rm "$lock_id" >/dev/null || true fi if [[ -n "$auth_dir" ]]; then rm -rf -- "$auth_dir" fi exit "$rc" } trap cleanup EXIT trap 'exit 130' INT trap 'exit 143' TERM trap 'exit 129' HUP "${compose[@]}" config --quiet "${compose[@]}" --profile jobs config --quiet auth_dir=$(mktemp -d) export DOCKER_CONFIG="$auth_dir" printf '%s' "$REGISTRY_PASSWORD" | docker login "${IMAGE_PREFIX%%/*}" --username "$REGISTRY_USERNAME" --password-stdin unset REGISTRY_PASSWORD # Download both images before stopping anything; migration uses the backend image. "${compose[@]}" pull --policy always backend web # Docker enforces unique container names across runner jobs and checkout paths. # The lock container is never started and receives no deployment credentials. if ! lock_id=$(docker create --name wq-alpha-production-deploy-lock \ --label "wq.deploy.commit=$DEPLOY_TAG" \ --network none --entrypoint /bin/true "$IMAGE_PREFIX-backend:$DEPLOY_TAG"); then echo 'Cannot acquire deployment lock; check Docker and other active deployments.' >&2 exit 1 fi # Validate secrets and DB connectivity before interrupting the running version. "${compose[@]}" run --rm --no-deps --pull never -T backend python -c ' import asyncio from app.config import Settings from sqlalchemy import text from sqlalchemy.ext.asyncio import create_async_engine async def check(): settings = Settings() engine = create_async_engine(settings.database_url) try: async with engine.connect() as connection: await connection.execute(text("SELECT 1")) finally: await engine.dispose() try: asyncio.run(check()) except Exception: raise SystemExit("Production configuration/database preflight failed; check env and database access.") from None ' # Record immutable image references before switching; do not prune old images. previous_images=$("${compose[@]}" images --quiet) if [[ -n "$previous_images" ]]; then echo "Previous deployment images (retain for rollback):" docker image inspect --format '{{.Id}} {{json .RepoTags}}' $previous_images fi "${compose[@]}" stop web backend "${compose[@]}" --profile jobs run --rm --no-deps --pull never -T migrate "${compose[@]}" up -d --no-build --pull never --remove-orphans --wait --wait-timeout 180 backend web echo "Production is healthy; release $DEPLOY_TAG"