#!/usr/bin/env bash set -euo pipefail root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) production_host=${AI_GATEWAY_PRODUCTION_HOST:-root@110.42.51.33} deploy_mode=${AI_GATEWAY_DEPLOY_MODE:-compose} case $deploy_mode in compose) default_remote_helper=/usr/local/sbin/easyai-ai-gateway-release ;; kubernetes) default_remote_helper=/usr/local/sbin/easyai-ai-gateway-cluster-release ;; *) echo 'AI_GATEWAY_DEPLOY_MODE must be compose or kubernetes' >&2; exit 1 ;; esac remote_helper=${AI_GATEWAY_REMOTE_RELEASE_HELPER:-$default_remote_helper} production_ssh_key=${AI_GATEWAY_PRODUCTION_SSH_KEY:-} action=deploy value= usage() { cat <<'EOF' Usage: scripts/deploy-production-release.sh dist/releases/.json scripts/deploy-production-release.sh --rollback scripts/deploy-production-release.sh --status This command changes production. Run it only after a successful publish command and an explicit user request to release or deploy to production. EOF } case ${1:-} in --status) action=status [[ $# -eq 1 ]] || { usage >&2; exit 64; } ;; --rollback) action=rollback [[ $# -eq 2 ]] || { usage >&2; exit 64; } value=$2 ;; -h|--help|'') usage [[ ${1:-} == -h || ${1:-} == --help ]] && exit 0 exit 64 ;; *) [[ $# -eq 1 ]] || { usage >&2; exit 64; } value=$1 ;; esac [[ $production_host =~ ^[A-Za-z0-9._-]+@[A-Za-z0-9.-]+$ ]] || { echo 'AI_GATEWAY_PRODUCTION_HOST must use user@host syntax' >&2 exit 1 } [[ $remote_helper =~ ^/[A-Za-z0-9._/-]+$ ]] || { echo 'AI_GATEWAY_REMOTE_RELEASE_HELPER must be an absolute safe path' >&2 exit 1 } ssh_options=(-o BatchMode=yes) scp_options=(-q -o BatchMode=yes) if [[ -n $production_ssh_key ]]; then [[ $production_ssh_key == /* && -f $production_ssh_key && ! -L $production_ssh_key ]] || { echo 'AI_GATEWAY_PRODUCTION_SSH_KEY must be an absolute regular file' >&2 exit 1 } ssh_options+=(-i "$production_ssh_key") scp_options+=(-i "$production_ssh_key") fi if [[ $action == status ]]; then exec ssh "${ssh_options[@]}" "$production_host" "$remote_helper status" fi if [[ $action == rollback ]]; then [[ $value =~ ^[0-9a-f]{40}$ ]] || { echo 'rollback release must be a full lowercase Git SHA' >&2 exit 1 } # shellcheck disable=SC2029 # Both arguments are validated safe tokens above. ssh "${ssh_options[@]}" "$production_host" "$remote_helper" rollback "$value" echo "production_rollback=PASS release=$value" exit 0 fi manifest=$(cd "$(dirname "$value")" 2>/dev/null && pwd)/$(basename "$value") [[ -f $manifest && ! -L $manifest ]] || { echo 'release manifest must be a regular file' >&2 exit 1 } node "$root/scripts/release-manifest.mjs" validate "$manifest" source_sha=$(node "$root/scripts/release-manifest.mjs" get "$manifest" sourceSha) base_sha=$(node "$root/scripts/release-manifest.mjs" get "$manifest" baseReleaseSha) remote_manifest=/tmp/easyai-ai-gateway-release-$source_sha.json status_file=$(mktemp) cleanup() { rm -f "$status_file" ssh "${ssh_options[@]}" "$production_host" rm -f -- "$remote_manifest" >/dev/null 2>&1 || true } trap cleanup EXIT HUP INT TERM if ssh "${ssh_options[@]}" "$production_host" "$remote_helper" status >"$status_file" 2>/dev/null; then node "$root/scripts/release-manifest.mjs" validate "$status_file" >/dev/null current_sha=$(node "$root/scripts/release-manifest.mjs" get "$status_file" sourceSha) [[ $current_sha == "$base_sha" ]] || { printf 'production base changed after publish: current=%s manifest_base=%s\n' \ "$current_sha" "$base_sha" >&2 exit 1 } else [[ -z $base_sha ]] || { echo 'production release status is unavailable for a non-bootstrap manifest' >&2 exit 1 } fi scp "${scp_options[@]}" "$manifest" "$production_host:$remote_manifest" # shellcheck disable=SC2029 # remote_manifest is derived from a validated full SHA. ssh "${ssh_options[@]}" "$production_host" "$remote_helper" deploy "$remote_manifest" echo "production_deploy=PASS release=$source_sha"