#!/usr/bin/env bash # One-time hosting setup for one service on Fly.io behind a Cloudflare Worker: the Fly app and its # volume, one fresh edge secret stored on both sides, and any extra secrets typed with hidden input. # Usage: deploy/bootstrap-hosting.sh [--dry-run] [--secrets-only] [--config FILE] # Settings come from deploy/hosting.env (KEY=value lines); FLY_ORG and FLY_REGION may be overridden. # Log in first with `fly auth login` and `npx wrangler@4.149.0 login`. # It prints names and next steps only, never a secret, and it deploys nothing. set -euo pipefail umask 077 WRANGLER_VERSION="4.149.0" HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO="$(cd "$HERE/.." && pwd)" CONFIG="$HERE/hosting.env" DRY_RUN=0 SECRETS_ONLY=0 FAILED=0 while [ $# -gt 0 ]; do case "$1" in --dry-run) DRY_RUN=1 ;; --secrets-only) SECRETS_ONLY=1 ;; --config) CONFIG="${2:?--config needs a file}"; shift ;; -h|--help) sed -n '2,7p' "$0"; exit 0 ;; *) echo "unknown option: $1 (try --help)" >&2; exit 2 ;; esac shift done # Read KEY=value lines without sourcing the file, so a settings file can never run code. Only FLY_ORG # and FLY_REGION may come from the environment; everything else comes from the file. KEYS="APP VOLUME VOLUME_GB FLY_ORG FLY_REGION EDGE_SECRET_NAME EDGE_CONFIG PROMPT_SECRETS CUSTOM_DOMAIN HEALTH_URL GITHUB_REPO" ENV_FLY_ORG="${FLY_ORG:-}" ENV_FLY_REGION="${FLY_REGION:-}" for key in $KEYS; do printf -v "$key" '%s' ""; done [ -f "$CONFIG" ] || { echo "settings file not found: $CONFIG" >&2; exit 2; } while IFS= read -r line || [ -n "$line" ]; do line="${line%%#*}" case "$line" in *=*) ;; *) continue ;; esac key="$(printf '%s' "${line%%=*}" | tr -d '[:space:]')" value="$(printf '%s' "${line#*=}" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')" case " $KEYS " in *" $key "*) printf -v "$key" '%s' "$value" ;; *) echo "ignoring unknown setting: $key" >&2 ;; esac done < "$CONFIG" for key in APP VOLUME EDGE_SECRET_NAME EDGE_CONFIG; do [ -n "${!key}" ] || { echo "$CONFIG must set $key" >&2; exit 2; } done VOLUME_GB="${VOLUME_GB:-1}" FLY_ORG="${ENV_FLY_ORG:-${FLY_ORG:-personal}}" FLY_REGION="${ENV_FLY_REGION:-${FLY_REGION:-iad}}" PROMPT_SECRETS="${PROMPT_SECRETS:-}" CUSTOM_DOMAIN="${CUSTOM_DOMAIN:-}" HEALTH_URL="${HEALTH_URL:-the Worker URL}" GITHUB_REPO="${GITHUB_REPO:-this repo}" EDGE_CONFIG_PATH="$REPO/$EDGE_CONFIG" for name in $EDGE_SECRET_NAME $PROMPT_SECRETS; do case "$name" in *[!A-Z0-9_]*) echo "secret names are A-Z, 0-9 and _ only: $name" >&2; exit 2 ;; esac done if [ -n "$CUSTOM_DOMAIN" ]; then case "$CUSTOM_DOMAIN" in *[!a-z0-9.-]*|.*|*.|*..*) echo "CUSTOM_DOMAIN must be a hostname like app.example.org: $CUSTOM_DOMAIN" >&2; exit 2 ;; esac fi wrangler() { npx --yes "wrangler@$WRANGLER_VERSION" "$@"; } plan() { cat <