#!/usr/bin/env bash set -euo pipefail ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd) SSH_TARGET=${PRODUCTION_SSH_TARGET:-whoneedhelp} REMOTE_ROOT=/srv/who_need_help-production REMOTE_ENV=$REMOTE_ROOT/.env EXPECTED_PROJECT=who_need_help_production EXPECTED_ORIGIN=https://whoneedhelp.com STATE_FILE="$ROOT/output/runtime/production-android-fcm-smoke.env" LOCAL_SCRIPT="$ROOT/scripts/production-android-fcm-smoke.exs" usage() { cat >&2 <<'EOF' Usage: ./scripts/production-android-fcm-smoke.sh plan DEVICE_ID --check-only whoneedhelp.com ./scripts/production-android-fcm-smoke.sh prepare DEVICE_ID --confirm whoneedhelp.com ./scripts/production-android-fcm-smoke.sh verify --from-state --confirm whoneedhelp.com ./scripts/production-android-fcm-smoke.sh cleanup --from-state --confirm whoneedhelp.com prepare sends one privacy-safe production notification to the exact active Android FCM device. verify proves the exact Oban delivery completed on its first attempt. cleanup removes only the run-scoped notification, job, manifest, and temporary script. The separate phases leave time to inspect the notification on the phone. No email worker, Web Push device, frozen test project, Caddy, or public Git is used. EOF exit 1 } read_remote_env() { local key=$1 ssh -o BatchMode=yes "$SSH_TARGET" \ "awk -F= -v key='$key' '\$1 == key {value = substr(\$0, index(\$0, \"=\") + 1); sub(/\\r\$/, \"\", value); if ((value ~ /^\".*\"\$/) || (value ~ /^\047.*\047\$/)) value = substr(value, 2, length(value) - 2); count++} END {if (count == 1) print value; else exit 1}' '$REMOTE_ENV'" } encode() { printf %s "$1" | base64 | tr -d '\n' } if [[ $# -ne 4 ]]; then usage fi ACTION=$1 DEVICE_ID=$2 CONFIRMATION=$3 HOST=$4 case "$ACTION" in plan) [[ "$CONFIRMATION" == --check-only ]] || usage ;; prepare) [[ "$CONFIRMATION" == --confirm ]] || usage ;; verify | cleanup) [[ "$DEVICE_ID" == --from-state && "$CONFIRMATION" == --confirm ]] || usage ;; *) usage ;; esac [[ "$HOST" == whoneedhelp.com ]] || usage if [[ "$ACTION" == plan || "$ACTION" == prepare ]]; then [[ "$DEVICE_ID" =~ ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$ ]] || usage fi if [[ ! -f "$LOCAL_SCRIPT" ]]; then echo "Local smoke script is missing: $LOCAL_SCRIPT" >&2 exit 1 fi DEPLOYMENT_ENV=$(read_remote_env DEPLOYMENT_ENV) DEPLOYMENT_TARGET=$(read_remote_env DEPLOYMENT_TARGET) PROJECT=$(read_remote_env COMPOSE_PROJECT_NAME) ORIGIN=$(read_remote_env WNH_BASE_URL) EXPECTED_DATABASE=$(read_remote_env POSTGRES_DB) EXPECTED_IMAGE=$(read_remote_env APP_IMAGE) if [[ "$DEPLOYMENT_ENV" != production || "$DEPLOYMENT_TARGET" != compose || "$PROJECT" != "$EXPECTED_PROJECT" || "$ORIGIN" != "$EXPECTED_ORIGIN" || -z "$EXPECTED_DATABASE" ]]; then echo "Remote deployment identity does not match the production target." >&2 exit 1 fi CONTAINER=$(ssh -o BatchMode=yes "$SSH_TARGET" \ "cd '$REMOTE_ROOT' && ./scripts/compose.sh '$REMOTE_ENV' ps -q app | head -n 1") if [[ -z "$CONTAINER" ]]; then echo "No running production app container was found." >&2 exit 1 fi read -r OBSERVED_IMAGE CONTAINER_STATE CONTAINER_HEALTH < <( ssh -o BatchMode=yes "$SSH_TARGET" \ "docker inspect --format '{{.Config.Image}} {{.State.Status}} {{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' '$CONTAINER'" ) if [[ "$OBSERVED_IMAGE" != "$EXPECTED_IMAGE" || "$CONTAINER_STATE" != running || "$CONTAINER_HEALTH" != healthy ]]; then echo "Production container identity or health does not match the environment." >&2 exit 1 fi ACTUAL_DATABASE=$(ssh -o BatchMode=yes "$SSH_TARGET" \ "docker exec '$CONTAINER' /app/bin/who_need_help rpc '%Postgrex.Result{rows: [[database]]} = WhoNeedHelp.Repo.query!(\"SELECT current_database()\", [], log: false); IO.puts(database)'" | tail -n 1) if [[ "$ACTUAL_DATABASE" != "$EXPECTED_DATABASE" ]]; then echo "Production database identity mismatch." >&2 exit 1 fi if [[ "$ACTION" == plan ]]; then printf 'scope=one production notification and one exact Android FCM delivery job\n' printf 'device_id=%s\ndatabase=%s\nimage=%s\ncontainer=%s\n' \ "$DEVICE_ID" "$ACTUAL_DATABASE" "$OBSERVED_IMAGE" "$CONTAINER" printf 'excluded=email delivery, Web Push, frozen test project, Caddy, public Git\n' printf 'cleanup=exact run-scoped notification, job, manifest, and temporary script\n' exit 0 fi mkdir -p "$ROOT/output/runtime" chmod 700 "$ROOT/output/runtime" umask 077 if [[ "$ACTION" == prepare ]]; then if [[ -e "$STATE_FILE" ]]; then echo "A prior Android FCM smoke state exists; inspect it before starting another run." >&2 exit 1 fi RUN_ID="$(date -u +%Y%m%d%H%M%S)-$(tr -d - "$STATE_FILE" <\"$REMOTE_SCRIPT\"'" <"$LOCAL_SCRIPT" else if [[ ! -f "$STATE_FILE" ]]; then echo "No Android FCM smoke state exists." >&2 exit 1 fi # shellcheck disable=SC1090 source "$STATE_FILE" if [[ "${schema_version:-}" != 1 || "${ssh_target:-}" != "$SSH_TARGET" || "${container:-}" != "$CONTAINER" || -z "${run_id:-}" || -z "${device_id:-}" || -z "${remote_script:-}" || -z "${remote_manifest:-}" ]]; then echo "Android FCM smoke state does not match the current production target." >&2 exit 1 fi RUN_ID=$run_id DEVICE_ID=$device_id REMOTE_SCRIPT=$remote_script REMOTE_MANIFEST=$remote_manifest fi RUN=$(encode "$RUN_ID") DATABASE=$(encode "$EXPECTED_DATABASE") DEVICE=$(encode "$DEVICE_ID") MANIFEST=$(encode "$REMOTE_MANIFEST") rpc_action() { local action=$1 local expression expression="Code.require_file(\"$REMOTE_SCRIPT\"); WhoNeedHelp.ProductionAndroidFCMSmoke.run(\"$action\", %{run_id: Base.decode64!(\"$RUN\"), expected_database: Base.decode64!(\"$DATABASE\"), device_id: Base.decode64!(\"$DEVICE\"), manifest_path: Base.decode64!(\"$MANIFEST\")})" ssh -o BatchMode=yes "$SSH_TARGET" \ "docker exec '$CONTAINER' /app/bin/who_need_help rpc '$expression'" } case "$ACTION" in prepare) if ! rpc_action prepare; then printf 'Preparation failed; state is preserved for exact inspection: %s\n' "$STATE_FILE" >&2 exit 1 fi printf 'state=%s\nnext=verify notification on the phone, then run verify and cleanup\n' "$STATE_FILE" ;; verify) rpc_action verify ;; cleanup) rpc_action cleanup ssh -o BatchMode=yes "$SSH_TARGET" \ "docker exec '$CONTAINER' rm -f '$REMOTE_SCRIPT' '$REMOTE_MANIFEST'" rm -f "$STATE_FILE" echo "production_android_fcm_smoke_cleanup_complete=true" ;; esac