#!/usr/bin/env bash set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" cd "$ROOT" PROJECT_ID="${RAILWAY_PROJECT_ID:-a6bc4ea7-e89c-42da-819a-8879fb022a0d}" ENVIRONMENT="${RAILWAY_ENVIRONMENT:-production}" ENV_FILE="${RAILWAY_ENV_FILE:-$ROOT/.env}" SERVICE_SELECTOR="all" SKIP_DEPLOYS=true DRY_RUN=false usage() { cat <<'USAGE' Sync selected environment variables from a local .env file to Railway services. Usage: scripts/railway-sync-env.sh [all|platform|extraction] [options] Options: --project Railway project ID (default: $RAILWAY_PROJECT_ID or script default) --env Railway environment name (default: $RAILWAY_ENVIRONMENT or production) --env-file Local env file to read (default: ./.env) --trigger-deploys Trigger deploys while setting vars (default is --skip-deploys) --dry-run Print variables that would be synced (no changes) -h, --help Show help Notes: - Missing optional vars are skipped. - Required vars (COSMOS_ENDPOINT, COSMOS_KEY, JWT_SECRET) must exist unless AZURE_KEYVAULT_URL is set. USAGE } if [[ $# -gt 0 && "${1#-}" == "$1" ]]; then SERVICE_SELECTOR="$1" shift fi while [[ $# -gt 0 ]]; do case "$1" in --project) PROJECT_ID="${2:-}" shift 2 ;; --env) ENVIRONMENT="${2:-}" shift 2 ;; --env-file) ENV_FILE="${2:-}" shift 2 ;; --trigger-deploys) SKIP_DEPLOYS=false shift ;; --dry-run) DRY_RUN=true shift ;; -h|--help) usage exit 0 ;; *) echo "Unknown arg: $1" >&2 usage exit 2 ;; esac done SERVICES=() case "$SERVICE_SELECTOR" in all) SERVICES=("platform-service" "extraction-service") ;; platform|platform-service) SERVICES=("platform-service") ;; extraction|extraction-service) SERVICES=("extraction-service") ;; *) echo "Unknown service selector: $SERVICE_SELECTOR" >&2 usage exit 2 ;; esac if [[ -z "$PROJECT_ID" ]]; then echo "Missing Railway project ID. Use --project or set RAILWAY_PROJECT_ID." >&2 exit 2 fi if [[ -z "$ENVIRONMENT" ]]; then echo "Missing Railway environment. Use --env or set RAILWAY_ENVIRONMENT." >&2 exit 2 fi if [[ ! -f "$ENV_FILE" ]]; then echo "Env file not found: $ENV_FILE" >&2 exit 2 fi if ! command -v railway >/dev/null 2>&1; then echo "railway CLI not found. Install with: npm i -g @railway/cli" >&2 exit 1 fi if [[ "$DRY_RUN" == false ]]; then if ! railway whoami >/dev/null 2>&1; then echo "Railway CLI is not authenticated. Run: railway login" >&2 exit 1 fi fi # Load .env values into current shell for lookup. # shellcheck disable=SC1090 set -a source "$ENV_FILE" set +a get_value() { local key="$1" printf '%s' "${!key-}" } has_value() { local key="$1" [[ -n "${!key-}" ]] } COMMON_REQUIRED=( COSMOS_ENDPOINT COSMOS_KEY JWT_SECRET ) COMMON_OPTIONAL=( AZURE_KEYVAULT_URL COSMOS_DATABASE DEFAULT_PRODUCT_ID CORS_ORIGIN PRODUCT_ID NODE_ENV HOST ) PLATFORM_OPTIONAL=( AZURE_BLOB_CONNECTION_STRING AZURE_BLOB_ACCOUNT_NAME AZURE_BLOB_ACCOUNT_KEY STRIPE_SECRET_KEY STRIPE_WEBHOOK_SECRET STRIPE_PRICE_PRO STRIPE_PRICE_ENTERPRISE BILLING_INTERNAL_KEY WEBHOOK_INVITATION_REDEEMED_URL WEBHOOK_REFERRAL_STATUS_URL BACKEND_URL PLAN_LIMITS_JSON USAGE_WARN_THRESHOLD RATE_LIMIT_CONFIG_JSON LICENSE_ACTIVATE_LOCKOUT_WINDOW_MS LICENSE_ACTIVATE_MAX_FAILED_ATTEMPTS COSMOS_AUTO_INIT ) EXTRACTION_OPTIONAL=( GEMINI_API_KEY DEFAULT_MODEL_ID PYTHON_SIDECAR_URL EXTRACTION_CACHE_TTL_MS EXTRACTION_CACHE_MAX EXTRACTION_CACHE_TTL EXTRACTION_CACHE_MAX_SIZE USE_MOCK_EXTRACTOR SIDECAR_PORT SIDECAR_HOST AZURE_OPENAI_KEY AZURE_OPENAI_ENDPOINT ) missing_required=() for key in "${COMMON_REQUIRED[@]}"; do if ! has_value "$key"; then missing_required+=("$key") fi done if [[ ${#missing_required[@]} -gt 0 && -z "$(get_value AZURE_KEYVAULT_URL)" ]]; then echo "Missing required variables in $ENV_FILE: ${missing_required[*]}" >&2 echo "Either set them in the env file, or set AZURE_KEYVAULT_URL and ensure Railway can access Key Vault." >&2 exit 2 fi if [[ ${#missing_required[@]} -gt 0 ]]; then echo "Warning: missing ${missing_required[*]} in $ENV_FILE; relying on Key Vault resolution at runtime." fi LINK_DIR="" cleanup() { if [[ -n "$LINK_DIR" && -d "$LINK_DIR" ]]; then rm -rf "$LINK_DIR" fi } trap cleanup EXIT run_railway() { if [[ "$DRY_RUN" == true ]]; then return 0 fi ( cd "$LINK_DIR" railway "$@" ) } if [[ "$DRY_RUN" == false ]]; then LINK_DIR="$(mktemp -d)" ( cd "$LINK_DIR" railway link --project "$PROJECT_ID" --environment "$ENVIRONMENT" >/dev/null ) fi ensure_service_exists() { local svc="$1" if [[ "$DRY_RUN" == true ]]; then return 0 fi if ! run_railway variable list --service "$svc" --environment "$ENVIRONMENT" --json >/dev/null 2>&1; then echo "Unable to access Railway service '$svc' in env '$ENVIRONMENT' (project '$PROJECT_ID')." >&2 echo "Check project/environment IDs and service names." >&2 exit 1 fi } set_variable() { local svc="$1" local key="$2" local value="$3" local -a cmd if [[ "$DRY_RUN" == true ]]; then echo " - would set $key" return 0 fi cmd=(variable set --service "$svc" --environment "$ENVIRONMENT") if [[ "$SKIP_DEPLOYS" == true ]]; then cmd+=(--skip-deploys) fi cmd+=("${key}=${value}") run_railway "${cmd[@]}" >/dev/null echo " - set $key" } sync_service() { local svc="$1" shift local keys=("$@") local value local seen="|" local synced=0 local skipped=0 echo "Syncing variables for $svc..." for key in "${keys[@]}"; do if [[ "$seen" == *"|$key|"* ]]; then continue fi seen+="$key|" value="$(get_value "$key")" if [[ -z "$value" ]]; then skipped=$((skipped + 1)) continue fi set_variable "$svc" "$key" "$value" synced=$((synced + 1)) done echo "Synced $synced variables to $svc (skipped $skipped unset keys)." } for svc in "${SERVICES[@]}"; do ensure_service_exists "$svc" if [[ "$svc" == "platform-service" ]]; then sync_service "$svc" \ "${COMMON_REQUIRED[@]}" \ "${COMMON_OPTIONAL[@]}" \ "${PLATFORM_OPTIONAL[@]}" else sync_service "$svc" \ "${COMMON_REQUIRED[@]}" \ "${COMMON_OPTIONAL[@]}" \ "${EXTRACTION_OPTIONAL[@]}" fi done if [[ "$SKIP_DEPLOYS" == true ]]; then echo "Variable sync complete (deploys were skipped)." echo "Run scripts/railway-deploy.sh to publish new code/config." else echo "Variable sync complete (deploys were triggered while setting vars)." fi