#!/usr/bin/env bash set -Eeuo pipefail unset DOCKER_HOST DOCKER_TLS_VERIFY DOCKER_CERT_PATH DOCKER_TLS_CERTDIR DEPLOY_DIR="${DEPLOY_DIR:-/home/gitlab-runner/deploy/call-center}" APP_IMAGE_NAME="${APP_IMAGE_NAME:-call-center-app}" APP_IMAGE_TAG="${APP_IMAGE_TAG:-${CI_COMMIT_SHORT_SHA:-latest}}" COMPOSE_FILE="${COMPOSE_FILE:-$DEPLOY_DIR/deployment/docker-compose.server.yml}" HEALTHCHECK_URL="${HEALTHCHECK_URL:-http://127.0.0.1:8080/health}" HEALTHCHECK_TIMEOUT_SECONDS="${HEALTHCHECK_TIMEOUT_SECONDS:-120}" require_command() { if ! command -v "$1" >/dev/null 2>&1; then echo "Missing required command: $1" >&2 exit 1 fi } wait_for_health() { python3 - "$HEALTHCHECK_URL" "$HEALTHCHECK_TIMEOUT_SECONDS" <<'PY' import sys import time import urllib.request url = sys.argv[1] timeout_seconds = int(sys.argv[2]) deadline = time.time() + timeout_seconds last_error = None while time.time() < deadline: try: with urllib.request.urlopen(url, timeout=5) as response: if response.getcode() == 200: print(f"Healthcheck passed: {url}") sys.exit(0) last_error = f"unexpected status {response.getcode()}" except Exception as exc: last_error = str(exc) time.sleep(3) print(f"Healthcheck failed for {url}: {last_error}", file=sys.stderr) sys.exit(1) PY } require_command docker require_command rsync require_command python3 repair_deploy_permissions() { if [[ ! -d "$DEPLOY_DIR" ]]; then return fi local target_uid local target_gid target_uid="$(id -u)" target_gid="$(id -g)" docker run --rm \ -e TARGET_UID="$target_uid" \ -e TARGET_GID="$target_gid" \ -v "$DEPLOY_DIR:/deploy" \ alpine:3.21 \ sh -lc ' set -eu chown "$TARGET_UID:$TARGET_GID" /deploy chmod u+rwx /deploy for path in /deploy/* /deploy/.[!.]* /deploy/..?*; do [ -e "$path" ] || continue case "$path" in /deploy/.env.production|/deploy/.env.production.*|\ /deploy/.data|/deploy/.data_gate3|/deploy/.data_gate4|\ /deploy/.data_uat_dry_run|/deploy/.data_uat_preflight|\ /deploy/.models|/deploy/.db_backups|/deploy/.deploy_backups) continue ;; esac chown -R "$TARGET_UID:$TARGET_GID" "$path" find "$path" -type d -exec chmod u+rwx {} + find "$path" -type f -exec chmod u+rw {} + done ' } if 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 "Missing docker compose or docker-compose" >&2 exit 1 fi if [[ -z "${CI_PROJECT_DIR:-}" || ! -d "${CI_PROJECT_DIR:-}" ]]; then echo "CI_PROJECT_DIR is not set or does not exist" >&2 exit 1 fi if ! docker info >/dev/null 2>&1; then echo "Docker daemon is not reachable for the runner user" >&2 exit 1 fi install -d "$DEPLOY_DIR" repair_deploy_permissions cd "$DEPLOY_DIR" if [[ "$CI_PROJECT_DIR" != "$DEPLOY_DIR" ]]; then rsync -a --delete \ --exclude '.git/' \ --exclude '.env.production' \ --exclude '.env.production.*' \ --exclude '.data/' \ --exclude '.data_local/' \ --exclude '.data_gate3/' \ --exclude '.data_gate4/' \ --exclude '.data_uat_dry_run/' \ --exclude '.data_uat_preflight/' \ --exclude '.venv/' \ --exclude '.pytest_cache/' \ --exclude '.local_stack/' \ --exclude '.models/' \ --exclude 'test-results/' \ --exclude '.codex_backup/' \ --exclude '.db_backups/' \ --exclude '.deploy_backups/' \ "$CI_PROJECT_DIR/" "$DEPLOY_DIR/" fi if [[ ! -f "$DEPLOY_DIR/.env.production" ]]; then if [[ -n "${DEPLOY_ENV_FILE:-}" && -f "${DEPLOY_ENV_FILE}" ]]; then install -m 600 "$DEPLOY_ENV_FILE" "$DEPLOY_DIR/.env.production" else echo "Missing $DEPLOY_DIR/.env.production. Create it once on the server or set DEPLOY_ENV_FILE." >&2 exit 1 fi fi install -d "$DEPLOY_DIR/.data_local" install -d "$DEPLOY_DIR/.data_local/generated_ivr_yandex/ivr" if [[ -d "$DEPLOY_DIR/.asterisk_assets/ivr" ]]; then rsync -a --delete \ "$DEPLOY_DIR/.asterisk_assets/ivr/" \ "$DEPLOY_DIR/.data_local/generated_ivr_yandex/ivr/" fi export CALL_CENTER_IMAGE="${APP_IMAGE_NAME}:${APP_IMAGE_TAG}" export COMPOSE_PROJECT_NAME="${COMPOSE_PROJECT_NAME:-call-center}" echo "Deploy directory: $DEPLOY_DIR" echo "Building image: $CALL_CENTER_IMAGE" docker build -t "$CALL_CENTER_IMAGE" -t "${APP_IMAGE_NAME}:latest" "$DEPLOY_DIR" if docker ps -a --format '{{.Names}}' | grep -Fxq 'call-center-api-gateway' \ && ! docker ps -a --format '{{.Names}}' | grep -Fxq 'call-center-app'; then echo "Removing legacy gateway container call-center-api-gateway before rename to call-center-app" docker rm -f call-center-api-gateway fi "${COMPOSE_CMD[@]}" -f "$COMPOSE_FILE" config -q "${COMPOSE_CMD[@]}" -f "$COMPOSE_FILE" up -d --remove-orphans --force-recreate wait_for_health docker ps --filter "name=^call-center-app$" --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}'