admin/scripts/deploy-finland-to-iran.sh
alisaza d3c48a1ed1 fix(ops): enable HTTPS backoffice and Sentry via Finland relay
Ship TLS nginx for backoffice, keep HTTP bootstrap until certs exist, proxy /monitoring to sentry-relay, and tunnel server/edge Sentry through Finland.
2026-09-13 20:38:19 +03:30

180 lines
7.6 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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
NGINX_BACKOFFICE=deploy/nginx/backoffice.conf
if ! "${ISSH[@]}" "${IRAN_USER}@${IRAN_HOST}" 'test -s /etc/letsencrypt/live/backoffice.ghabilee.ir/fullchain.pem'; then
if [[ -f deploy/nginx/backoffice-http-bootstrap.conf ]]; then
NGINX_BACKOFFICE=deploy/nginx/backoffice-http-bootstrap.conf
fi
fi
scp -i "$HOME/.ssh/iran_key" -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new \
"$NGINX_BACKOFFICE" \
"${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