who_need_help/scripts/production-android-fcm-smoke.sh

202 lines
6.8 KiB
Bash
Executable File

#!/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 - </proc/sys/kernel/random/uuid | cut -c1-12)"
REMOTE_SCRIPT="/tmp/wnh-production-android-fcm-$RUN_ID.exs"
REMOTE_MANIFEST="/tmp/wnh-production-android-fcm-$RUN_ID.json"
cat >"$STATE_FILE" <<EOF
schema_version=1
run_id=$RUN_ID
ssh_target=$SSH_TARGET
container=$CONTAINER
device_id=$DEVICE_ID
remote_script=$REMOTE_SCRIPT
remote_manifest=$REMOTE_MANIFEST
EOF
chmod 600 "$STATE_FILE"
ssh -o BatchMode=yes "$SSH_TARGET" \
"docker exec -i '$CONTAINER' sh -c 'umask 077; cat >\"$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