#!/usr/bin/env bash set -euo pipefail # ───────────────────────── config ───────────────────────── ENV="${1:-${ENV:-dev}}" case "$ENV" in dev|staging|prod) ;; *) echo "❌ Unknown ENV='$ENV'"; exit 1 ;; esac PROJECT="aptivaai-${ENV}" REG="us-central1-docker.pkg.dev/${PROJECT}/aptiva-repo" ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" MIRROR_TO_STAGING="${MIRROR_TO_STAGING:-false}" # default off (set via Woodpecker var) MIRROR_TO_PROD="${MIRROR_TO_PROD:-false}" # default off PROMOTE_PROD="${PROMOTE_PROD:-false}" # default off echo "🔧 Deploying environment: $ENV (GCP: $PROJECT)" SECRETS=( ENV_NAME PROJECT CORS_ALLOWED_ORIGINS TOKEN_MAX_AGE_MS COOKIE_SECURE COOKIE_SAMESITE ACCESS_COOKIE_NAME SERVER1_PORT SERVER2_PORT SERVER3_PORT JWT_SECRET OPENAI_API_KEY ONET_USERNAME ONET_PASSWORD STRIPE_SECRET_KEY STRIPE_PUBLISHABLE_KEY STRIPE_WH_SECRET STRIPE_PRICE_PREMIUM_MONTH STRIPE_PRICE_PREMIUM_YEAR STRIPE_PRICE_PRO_MONTH STRIPE_PRICE_PRO_YEAR DB_HOST DB_NAME DB_PORT DB_USER DB_PASSWORD DB_SSL_CERT DB_SSL_KEY DB_SSL_CA SUPPORT_SENDGRID_API_KEY EMAIL_INDEX_SECRET APTIVA_API_BASE TWILIO_ACCOUNT_SID TWILIO_AUTH_TOKEN TWILIO_MESSAGING_SERVICE_SID GOOGLE_MAPS_API_KEY KMS_KEY_NAME DEK_PATH ) cd "$ROOT" # ───────────── pull runtime secrets (BEFORE build) ───────────── echo "🔐 Pulling secrets from Secret Manager" for S in "${SECRETS[@]}"; do export "$S"="$(gcloud secrets versions access latest --secret="${S}_${ENV}" --project="$PROJECT")" done export FROM_SECRETS_MANAGER=true # React needs the prefixed var at BUILD time export REACT_APP_GOOGLE_MAPS_API_KEY="$GOOGLE_MAPS_API_KEY" export REACT_APP_ENV_NAME="$ENV_NAME" # ───────────────────────── node + npm ci cache ───────────────────────── echo "🛠 Building front-end bundle (skips when unchanged)" export npm_config_cache="${HOME}/.npm" # persist npm cache export CI=false # don’t treat warnings as errors NODE_VER="$(node -v 2>/dev/null || echo 'none')" if [[ ! -f .last-node || "$(cat .last-node 2>/dev/null || echo)" != "$NODE_VER" ]]; then echo "♻️ Node changed → cleaning node_modules (was '$(cat .last-node 2>/dev/null || echo none)', now '${NODE_VER}')" rm -rf node_modules .build.hash fi echo "$NODE_VER" > .last-node if [[ ! -f package-lock.json ]]; then echo "⚠️ package-lock.json missing; running npm ci" npm ci --silent --no-audit --no-fund else LOCK_HASH="$(sha1sum package-lock.json | awk '{print $1}')" if [[ -d node_modules && -f .last-lock && "$(cat .last-lock)" == "$LOCK_HASH" ]]; then echo "📦 node_modules up-to-date; skipping npm ci" else echo "📦 installing deps…" npm ci --silent --no-audit --no-fund echo "$LOCK_HASH" > .last-lock echo "$LOCK_HASH" > .lock.hash # legacy compat fi fi # ───────────────────────── npm run build cache ───────────────────────── SRC_HASH="$(find src public -type f -print0 2>/dev/null | sort -z | xargs -0 sha1sum | sha1sum | awk '{print $1}')" PKG_HASH="$(sha1sum package.json package-lock.json 2>/dev/null | sha1sum | awk '{print $1}')" BUILD_ENV_HASH="$(printf '%s' "${REACT_APP_GOOGLE_MAPS_API_KEY}-${REACT_APP_API_URL:-}" | sha1sum | awk '{print $1}')" COMBINED_HASH="${SRC_HASH}-${PKG_HASH}-${BUILD_ENV_HASH}" if [[ -f .build.hash && "$(cat .build.hash)" == "$COMBINED_HASH" && -d build ]]; then echo "🏗 static bundle up-to-date; skipping npm run build" else echo "🏗 Building static bundle…" GENERATE_SOURCEMAP=false NODE_OPTIONS="--max-old-space-size=4096" npm run build echo "$COMBINED_HASH" > .build.hash fi # ───────────────────── build & push images (SEQUENTIAL) ───────────────────── export DOCKER_BUILDKIT=1 export COMPOSE_DOCKER_CLI_BUILD=1 export BUILDKIT_PROGRESS=plain # stable progress output TAG="$(git rev-parse --short HEAD)-$(date -u +%Y%m%d%H%M)" echo "🔨 Building & pushing containers (tag = ${TAG})" build_and_push () { local svc="$1" echo "🧱 Building ${svc}…" docker build --progress=plain -f "Dockerfile.${svc}" -t "${REG}/${svc}:${TAG}" . echo "⏫ Pushing ${svc}…" docker push "${REG}/${svc}:${TAG}" } SERVICES=(server1 server2 server3 nginx) # Build & push to DEV registry first (source of truth) for svc in "${SERVICES[@]}"; do build_and_push "$svc" done # ───────────────────── optional: mirror to staging/prod ───────────────────── # Staging mirror if [[ "$MIRROR_TO_STAGING" == "true" ]]; then echo "🔁 Mirroring images to STAGING registry (tag=${TAG})" DST_REG_STG="us-central1-docker.pkg.dev/aptivaai-staging/aptiva-repo" for svc in "${SERVICES[@]}"; do docker tag "${REG}/${svc}:${TAG}" "${DST_REG_STG}/${svc}:${TAG}" docker push "${DST_REG_STG}/${svc}:${TAG}" done printf "%s" "${TAG}" | gcloud secrets versions add IMG_TAG --data-file=- --project="aptivaai-staging" >/dev/null echo "🏷 IMG_TAG published to aptivaai-staging" else echo "⏭ Skipping staging mirror (MIRROR_TO_STAGING=$MIRROR_TO_STAGING)" fi # Prod mirror (dual-key: MIRROR_TO_PROD && PROMOTE_PROD) if [[ "$MIRROR_TO_PROD" == "true" && "$PROMOTE_PROD" == "true" ]]; then echo "🔁 Mirroring images to PROD registry (tag=${TAG})" DST_REG_PROD="us-central1-docker.pkg.dev/aptivaai-prod/aptiva-repo" for svc in "${SERVICES[@]}"; do docker tag "${REG}/${svc}:${TAG}" "${DST_REG_PROD}/${svc}:${TAG}" docker push "${DST_REG_PROD}/${svc}:${TAG}" done printf "%s" "${TAG}" | gcloud secrets versions add IMG_TAG --data-file=- --project="aptivaai-prod" >/dev/null echo "🏷 IMG_TAG published to aptivaai-prod" else echo "⏭ Skipping prod mirror (MIRROR_TO_PROD=$MIRROR_TO_PROD, PROMOTE_PROD=$PROMOTE_PROD)" fi # ───────────────────── write IMG_TAG locally ───────────────────── export IMG_TAG="${TAG}" echo "🔖 Using IMG_TAG=${IMG_TAG}" # ───────────────────── publish IMG_TAG to Secret Manager ───────────────────── printf "%s" "${TAG}" | gcloud secrets versions add IMG_TAG --data-file=- --project="$PROJECT" >/dev/null echo "📦 IMG_TAG pushed to Secret Manager" # ───────────────────── docker compose up ───────────────────── preserve=IMG_TAG,FROM_SECRETS_MANAGER,REACT_APP_API_URL,REACT_APP_GOOGLE_MAPS_API_KEY,$(IFS=,; echo "${SECRETS[*]}") echo "🚀 docker compose up -d (env: $preserve)" sudo --preserve-env="$preserve" docker compose up -d --force-recreate \ 2> >(grep -v 'WARN \[0000\]') echo "✅ Deployment finished"