#!/usr/bin/env bash # Build admin image on the Finland host, then load+run it on Iran. # # Architecture: # - Finland (git.ghabilee.ir / act_runner): build + Telegram notify # - Iran: runtime only (/opt/ghabilee-admin) set -euo pipefail : "${FINLAND_SSH_KEY:?}" : "${FINLAND_HOST:?}" : "${FINLAND_USER:?}" : "${IRAN_SSH_KEY:?}" : "${IRAN_HOST:?}" : "${IRAN_USER:?}" ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" cd "$ROOT" IMAGE_TAG="${IMAGE_TAG:-ghabilee-admin:$(git rev-parse --short HEAD 2>/dev/null || date +%s)}" FINLAND_APP="/opt/ghabilee-admin-ci" IRAN_APP="/opt/ghabilee-admin" install -m 700 -d "$HOME/.ssh" printf '%s\n' "$FINLAND_SSH_KEY" > "$HOME/.ssh/finland_key" printf '%s\n' "$IRAN_SSH_KEY" > "$HOME/.ssh/iran_key" chmod 600 "$HOME/.ssh/finland_key" "$HOME/.ssh/iran_key" trap 'rm -f "$HOME/.ssh/finland_key" "$HOME/.ssh/iran_key"' EXIT FSSH=(ssh -i "$HOME/.ssh/finland_key" -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new) ISSH=(ssh -i "$HOME/.ssh/iran_key" -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new) FRSYNC=(-e "ssh -i $HOME/.ssh/finland_key -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new") # --- build env from Iran (runtime .env holds NEXT_PUBLIC_* bake inputs) --- "${ISSH[@]}" "${IRAN_USER}@${IRAN_HOST}" "test -s ${IRAN_APP}/.env" scp -i "$HOME/.ssh/iran_key" -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new \ "${IRAN_USER}@${IRAN_HOST}:${IRAN_APP}/.env" .env.production test -s .env.production # --- sync sources + env to Finland build dir --- "${FSSH[@]}" "${FINLAND_USER}@${FINLAND_HOST}" "install -d -m 0750 '${FINLAND_APP}/src'" rsync -az --delete "${FRSYNC[@]}" \ --exclude '.git' \ --exclude 'node_modules' \ --exclude '.next' \ --exclude '.env' \ --exclude '.env.*' \ --exclude 'test-results' \ --exclude 'playwright-report' \ ./ "${FINLAND_USER}@${FINLAND_HOST}:${FINLAND_APP}/src/" scp -i "$HOME/.ssh/finland_key" -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new \ .env.production "${FINLAND_USER}@${FINLAND_HOST}:${FINLAND_APP}/.env" rm -f .env.production # --- build on Finland (enough RAM; Telegram/git live here too) --- "${FSSH[@]}" "${FINLAND_USER}@${FINLAND_HOST}" \ "IMAGE_TAG='${IMAGE_TAG}' APP_DIR='${FINLAND_APP}' SRC_DIR='${FINLAND_APP}/src' bash -s" <<'REMOTE' set -euo pipefail cd "$APP_DIR" # noglob: backend-style cron values must never expand if present in .env set -f # shellcheck disable=SC1091 set -a # shellcheck disable=SC1090 source "$APP_DIR/.env" set +a set +f env_or_empty() { printf '%s' "${!1-}"; } sanitize_api_proxy_target() { local target target="$(env_or_empty API_PROXY_TARGET)" case "$target" in *127.0.0.1*|*localhost*|*'::1'*) printf '' ;; *) printf '%s' "$target" ;; esac } # DOCKER_BUILDKIT keeps layer/build cache across deploys; never prune -af here. export DOCKER_BUILDKIT=1 docker build --platform linux/amd64 -t "$IMAGE_TAG" \ --build-arg "NEXT_PUBLIC_API_URL=$(env_or_empty NEXT_PUBLIC_API_URL)" \ --build-arg "NEXT_PUBLIC_FILE_SERVER_URL=$(env_or_empty NEXT_PUBLIC_FILE_SERVER_URL)" \ --build-arg "MAP_API_KEY=$(env_or_empty MAP_API_KEY)" \ --build-arg "NEXT_PUBLIC_MAP_API_KEY=$(env_or_empty NEXT_PUBLIC_MAP_API_KEY)" \ --build-arg "NEXT_PUBLIC_VAPID_PUBLIC_KEY=$(env_or_empty NEXT_PUBLIC_VAPID_PUBLIC_KEY)" \ --build-arg "NEXT_PUBLIC_BASE_PATH=$(env_or_empty NEXT_PUBLIC_BASE_PATH)" \ --build-arg "NEXT_PUBLIC_SITE_URL=$(env_or_empty NEXT_PUBLIC_SITE_URL)" \ --build-arg "NEXT_PUBLIC_OBSERVABILITY_ENDPOINT=$(env_or_empty NEXT_PUBLIC_OBSERVABILITY_ENDPOINT)" \ --build-arg "NEXT_PUBLIC_SENTRY_DSN=$(env_or_empty NEXT_PUBLIC_SENTRY_DSN)" \ --build-arg "NEXT_PUBLIC_SENTRY_ENVIRONMENT=$(env_or_empty NEXT_PUBLIC_SENTRY_ENVIRONMENT)" \ --build-arg "NEXT_PUBLIC_SENTRY_TRACES_SAMPLE_RATE=$(env_or_empty NEXT_PUBLIC_SENTRY_TRACES_SAMPLE_RATE)" \ --build-arg "NEXT_PUBLIC_ARCAPTCHA_SITE_KEY=$(env_or_empty NEXT_PUBLIC_ARCAPTCHA_SITE_KEY)" \ --build-arg "SENTRY_AUTH_TOKEN=$(env_or_empty SENTRY_AUTH_TOKEN)" \ --build-arg "SENTRY_ORG=$(env_or_empty SENTRY_ORG)" \ --build-arg "SENTRY_PROJECT=$(env_or_empty SENTRY_PROJECT)" \ --build-arg "API_PROXY_TARGET=$(sanitize_api_proxy_target)" \ "$SRC_DIR" # فقط dangling؛ prune -a/-af کش build را می‌کشد و هر deploy را cold می‌کند docker image prune -f >/dev/null 2>&1 || true echo "BUILT $IMAGE_TAG" REMOTE # --- stream image Finland → Iran, then compose up --- "${ISSH[@]}" "${IRAN_USER}@${IRAN_HOST}" "install -d -m 0750 '${IRAN_APP}'" scp -i "$HOME/.ssh/iran_key" -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new \ deploy/docker-compose.production.yml \ "${IRAN_USER}@${IRAN_HOST}:${IRAN_APP}/docker-compose.yml" if [[ -f deploy/nginx/backoffice.conf ]]; then scp -i "$HOME/.ssh/iran_key" -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new \ deploy/nginx/backoffice.conf \ "${IRAN_USER}@${IRAN_HOST}:/etc/nginx/sites-available/backoffice" "${ISSH[@]}" "${IRAN_USER}@${IRAN_HOST}" ' ln -sfn /etc/nginx/sites-available/backoffice /etc/nginx/sites-enabled/backoffice nginx -t && systemctl reload nginx ' fi # فشرده‌سازی transfer؛ zstd اگر روی هر دو host باشد، وگرنه gzip TRANSFER_CODEC=gzip if "${FSSH[@]}" "${FINLAND_USER}@${FINLAND_HOST}" 'command -v zstd >/dev/null 2>&1' \ && "${ISSH[@]}" "${IRAN_USER}@${IRAN_HOST}" 'command -v zstd >/dev/null 2>&1'; then TRANSFER_CODEC=zstd fi echo "Transferring ${IMAGE_TAG} Finland → Iran (${TRANSFER_CODEC})..." if [[ "$TRANSFER_CODEC" == zstd ]]; then "${FSSH[@]}" "${FINLAND_USER}@${FINLAND_HOST}" "docker save '${IMAGE_TAG}' | zstd -T0 -3" \ | "${ISSH[@]}" "${IRAN_USER}@${IRAN_HOST}" 'zstd -d | docker load' else "${FSSH[@]}" "${FINLAND_USER}@${FINLAND_HOST}" "docker save '${IMAGE_TAG}' | gzip -1" \ | "${ISSH[@]}" "${IRAN_USER}@${IRAN_HOST}" 'gzip -d | docker load' fi "${ISSH[@]}" "${IRAN_USER}@${IRAN_HOST}" \ "ADMIN_IMAGE='${IMAGE_TAG}' bash -s" <<'REMOTE' set -euo pipefail cd /opt/ghabilee-admin test -s .env ADMIN_IMAGE="$ADMIN_IMAGE" docker compose -f docker-compose.yml up -d --no-deps ghabilee-admin for attempt in $(seq 1 36); do health="$(docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' ghabilee-admin 2>/dev/null || echo missing)" echo "Admin health ${attempt}/36: ${health}" if [ "$health" = healthy ]; then # آزاد کردن دیسک بدون نابود کردن کش: فقط tagهای قدیمی admin که در حال استفاده نیستند docker images --format '{{.Repository}}:{{.Tag}}' \ | awk -v keep="$ADMIN_IMAGE" '$0 ~ /^ghabilee-admin:/ && $0 != keep { print }' \ | while IFS= read -r old; do docker rmi "$old" >/dev/null 2>&1 || true done docker image prune -f >/dev/null 2>&1 || true echo "Deploy OK" exit 0 fi case "$health" in unhealthy|exited|dead|missing) exit 1 ;; esac sleep 5 done exit 1 REMOTE # روی Finland هم tagهای قدیمی admin را بردار؛ BuildKit cache دست‌نخورده می‌ماند "${FSSH[@]}" "${FINLAND_USER}@${FINLAND_HOST}" \ "IMAGE_TAG='${IMAGE_TAG}' bash -s" <<'REMOTE' set -euo pipefail docker images --format '{{.Repository}}:{{.Tag}}' \ | awk -v keep="$IMAGE_TAG" '$0 ~ /^ghabilee-admin:/ && $0 != keep { print }' \ | while IFS= read -r old; do docker rmi "$old" >/dev/null 2>&1 || true done docker image prune -f >/dev/null 2>&1 || true REMOTE