#!/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-web-push-smoke.env" LOCAL_SCRIPT="$ROOT/scripts/production-web-push-smoke.exs" usage() { cat >&2 <<'EOF' Usage: ./scripts/production-web-push-smoke.sh plan USER_EMAIL --check-only whoneedhelp.com ./scripts/production-web-push-smoke.sh run USER_EMAIL --confirm whoneedhelp.com run sends one browser-only production Web Push notification to the newest active Web Push device for USER_EMAIL, verifies the exact Oban delivery completed on its first attempt, then removes the run-scoped notification, job, and temporary files. It never invokes the notification email worker or the Android FCM device. 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 USER_EMAIL=${2,,} CONFIRMATION=$3 HOST=$4 case "$ACTION" in plan) [[ "$CONFIRMATION" == --check-only ]] || usage ;; run) [[ "$CONFIRMATION" == --confirm ]] || usage ;; *) usage ;; esac [[ "$HOST" == whoneedhelp.com ]] || usage if [[ ! "$USER_EMAIL" =~ ^[^[:space:]@]+@[^[:space:]@]+$ ]]; then echo "USER_EMAIL is invalid." >&2 exit 1 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 browser-only Web Push delivery job\n' printf 'target=%s\ndatabase=%s\nimage=%s\ncontainer=%s\n' \ "$USER_EMAIL" "$ACTUAL_DATABASE" "$OBSERVED_IMAGE" "$CONTAINER" printf 'excluded=email delivery, Android FCM, frozen test project, Caddy, public Git\n' printf 'cleanup=exact run-scoped notification, job, manifest, and temporary script\n' exit 0 fi if [[ -e "$STATE_FILE" ]]; then echo "A prior Web Push smoke state exists; inspect it before starting another run." >&2 exit 1 fi mkdir -p "$ROOT/output/runtime" chmod 700 "$ROOT/output/runtime" umask 077 RUN_ID="$(date -u +%Y%m%d%H%M%S)-$(tr -d - "$STATE_FILE" </dev/null 2>&1 || true rm -f "$STATE_FILE" } preserve_failed_run() { local status=$1 trap - EXIT INT TERM if [[ "$cleanup_complete" == true ]]; then remove_temporary_files else printf '%s\n' \ "Web Push smoke did not complete exact record cleanup." \ "Run-scoped state was preserved for inspection:" \ " local state: $STATE_FILE" \ " remote manifest: $REMOTE_MANIFEST" \ " remote script: $REMOTE_SCRIPT" >&2 fi exit "$status" } trap 'preserve_failed_run $?' EXIT trap 'preserve_failed_run 130' INT trap 'preserve_failed_run 143' TERM ssh -o BatchMode=yes "$SSH_TARGET" \ "docker exec -i '$CONTAINER' sh -c 'umask 077; cat >\"$REMOTE_SCRIPT\"'" <"$LOCAL_SCRIPT" RUN=$(encode "$RUN_ID") DATABASE=$(encode "$EXPECTED_DATABASE") EMAIL=$(encode "$USER_EMAIL") MANIFEST=$(encode "$REMOTE_MANIFEST") rpc_action() { local action=$1 local expression expression="Code.require_file(\"$REMOTE_SCRIPT\"); WhoNeedHelp.ProductionWebPushSmoke.run(\"$action\", %{run_id: Base.decode64!(\"$RUN\"), expected_database: Base.decode64!(\"$DATABASE\"), user_email: Base.decode64!(\"$EMAIL\"), manifest_path: Base.decode64!(\"$MANIFEST\")})" ssh -o BatchMode=yes "$SSH_TARGET" \ "docker exec '$CONTAINER' /app/bin/who_need_help rpc '$expression'" } rpc_action prepare verified=false for _attempt in $(seq 1 20); do if rpc_action verify; then verified=true break fi sleep 1 done if [[ "$verified" != true ]]; then echo "Web Push provider delivery did not verify within 20 seconds." >&2 echo "Run-scoped state remains in production for inspection; automatic record deletion was not attempted." >&2 exit 1 fi rpc_action cleanup cleanup_complete=true remove_temporary_files trap - EXIT INT TERM echo "production_web_push_smoke_complete=true"