#!/bin/sh set -eu set +x : "${PUBLIC_WEB_ORIGIN:?PUBLIC_WEB_ORIGIN is required}" : "${PRODUCTION_IMAGE_REPOSITORY:?PRODUCTION_IMAGE_REPOSITORY is required}" : "${TARGET_SSH_KEY:?}" : "${TARGET_SSH_USER:?}" : "${TARGET_SSH_HOST:?}" : "${TARGET_SSH_KNOWN_HOSTS:?}" : "${BUILD_REGISTRY_USER:?}" : "${BUILD_REGISTRY_PASSWORD:?}" : "${REGISTRY_USER:?}" : "${REGISTRY_PASSWORD:?}" case "$PUBLIC_WEB_ORIGIN" in https://*) ;; *) echo 'prod requires HTTPS web origin' >&2; exit 2 ;; esac # Only an origin (optionally a trailing slash) is accepted; no shell/env-file metacharacters. printf '%s' "$PUBLIC_WEB_ORIGIN" | grep -Eq '^https://[A-Za-z0-9.-]+(:[0-9]+)?/?$' printf '%s' "$PRODUCTION_IMAGE_REPOSITORY" | grep -Eq '^[A-Za-z0-9.-]+(:[0-9]+)?/[a-z0-9._/-]+$' git fetch --no-tags origin +refs/heads/main:refs/remotes/origin/main test "$(git rev-parse HEAD)" = "$(git rev-parse origin/main)" IMAGE_TAG=$(git rev-parse --short=12 HEAD) registry=${PRODUCTION_IMAGE_REPOSITORY%%/*} image="$PRODUCTION_IMAGE_REPOSITORY:$IMAGE_TAG" TARGET_SSH_TARGET="$TARGET_SSH_USER@$TARGET_SSH_HOST" remote_dir=/data/firstleg/deploy/gray-client-web run_ssh() { ssh -i "$TARGET_SSH_KEY" -o BatchMode=yes -o StrictHostKeyChecking=yes \ -o UserKnownHostsFile="$TARGET_SSH_KNOWN_HOSTS" -o ConnectTimeout=12 "$TARGET_SSH_TARGET" "$@" } run_scp() { scp -i "$TARGET_SSH_KEY" -o BatchMode=yes -o StrictHostKeyChecking=yes \ -o UserKnownHostsFile="$TARGET_SSH_KNOWN_HOSTS" -o ConnectTimeout=12 "$@" } export DOCKER_CONFIG DOCKER_CONFIG=$(mktemp -d) cleanup() { rm -f "$DOCKER_CONFIG/config.json"; rmdir "$DOCKER_CONFIG" 2>/dev/null || true; } trap cleanup EXIT HUP INT TERM printf '%s' "$BUILD_REGISTRY_PASSWORD" | docker login -u "$BUILD_REGISTRY_USER" --password-stdin "$registry" docker build --build-arg NODE_IMAGE="${NODE_BASE_IMAGE:-node:22-alpine}" \ --build-arg NGINX_IMAGE="${NGINX_BASE_IMAGE:-nginx:1.27-alpine}" \ --build-arg VITE_ADMIN_WEB_ORIGIN="$PUBLIC_WEB_ORIGIN" -t "$image" . docker push "$image" run_ssh install -d -m 750 "$remote_dir" run_scp docker-compose.production.yml "$TARGET_SSH_TARGET:$remote_dir/compose.yml" # The remote env file contains only image coordinates and public web origin, never credentials. printf 'PRODUCTION_IMAGE_REPOSITORY=%s\nIMAGE_TAG=%s\nPUBLIC_WEB_ORIGIN=%s\n' \ "$PRODUCTION_IMAGE_REPOSITORY" "$IMAGE_TAG" "$PUBLIC_WEB_ORIGIN" | run_ssh sh -c "'umask 077; cat > $remote_dir/release.env'" # Registry credentials exist only in an isolated temporary Docker config, removed after pull. printf '%s' "$REGISTRY_USER" | grep -Eq '^[A-Za-z0-9_.@-]+$' run_scp scripts/pull-production-image.sh "$TARGET_SSH_TARGET:$remote_dir/pull-image.sh" printf '%s' "$REGISTRY_PASSWORD" | run_ssh sh "$remote_dir/pull-image.sh" "$REGISTRY_USER" "$registry" "$image" run_ssh sh -s -- "$remote_dir" <<'REMOTE' set -eu cd "$1" docker network inspect firstleg-production-network >/dev/null docker compose --env-file release.env -f compose.yml config --quiet docker compose --env-file release.env -f compose.yml up -d --wait --wait-timeout 120 docker compose --env-file release.env -f compose.yml exec -T client-web wget -qO- http://127.0.0.1/healthz REMOTE