#!/usr/bin/env bash set -euo pipefail ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd) env_file=${1:-"$ROOT/.env"} mode=${2:-} if [[ "$env_file" != /* ]]; then env_file="$ROOT/$env_file" fi if [[ "$mode" != "" && "$mode" != "--require-release" ]]; then echo "Usage: $0 [ENV_FILE] [--require-release]" >&2 exit 2 fi if [[ ! -f "$env_file" ]]; then echo "Environment file does not exist: $env_file" >&2 exit 2 fi if [[ "$(stat -c '%a' "$env_file")" != 600 ]]; then echo "Environment file must have mode 0600: $env_file" >&2 exit 2 fi read_value() { local key=$1 awk -v key="$key" ' index($0, key "=") == 1 { value = substr($0, length(key) + 2) if (value ~ /^".*"$/ || value ~ /^'\''.*'\''$/) { value = substr(value, 2, length(value) - 2) } print value found = 1 exit } END { if (!found) exit 1 } ' "$env_file" } value() { read_value "$1" 2>/dev/null || true } is_set() { [[ -n "$(value "$1")" ]] } all_set() { local key for key in "$@"; do is_set "$key" || return 1 done } all_empty() { local key for key in "$@"; do is_set "$key" && return 1 done return 0 } contains_template_marker() { local observed=$1 [[ "$observed" == *REPLACE* || "$observed" == *GENERATE* || "$observed" == *example.com* || "$observed" == *example.invalid* ]] } valid_fcm_service_account_json() { jq -e ' .type == "service_account" and (.project_id | type == "string" and length > 0) and (.client_email | type == "string" and length > 0) and (.private_key | type == "string" and length > 0) ' >/dev/null 2>&1 } fcm_service_account_project_id() { jq -er ' select( .type == "service_account" and (.project_id | type == "string" and length > 0) and (.client_email | type == "string" and length > 0) and (.private_key | type == "string" and length > 0) ) | .project_id ' 2>/dev/null } firebase_client_values_valid() { local application_id sender_id prefix application_id=$(value WNH_FIREBASE_APPLICATION_ID) sender_id=$(value WNH_FIREBASE_GCM_SENDER_ID) prefix="1:$sender_id:android:" [[ "$sender_id" =~ ^[0-9]+$ && "$application_id" == "$prefix"* && -n "${application_id#"$prefix"}" ]] } valid_sha256_fingerprint_list() { local fingerprint compact local -a fingerprint_list IFS=',' read -r -a fingerprint_list <<<"$1" [[ ${#fingerprint_list[@]} -gt 0 ]] || return 1 for fingerprint in "${fingerprint_list[@]}"; do compact=${fingerprint//:/} compact=${compact//[[:space:]]/} [[ "$compact" =~ ^[0-9A-Fa-f]{64}$ ]] || return 1 done } failures=0 warnings=0 ready() { printf 'READY %-24s %s\n' "$1" "$2" } local_only() { printf 'LOCAL_ONLY %-24s %s\n' "$1" "$2" warnings=$((warnings + 1)) } missing() { printf 'MISSING %-24s %s\n' "$1" "$2" failures=$((failures + 1)) } invalid() { printf 'INVALID %-24s %s\n' "$1" "$2" failures=$((failures + 1)) } partial() { printf 'PARTIAL %-24s %s\n' "$1" "$2" failures=$((failures + 1)) } deployment_env=$(value DEPLOYMENT_ENV) phx_host=$(value PHX_HOST) phx_scheme=$(value PHX_SCHEME) phx_port=$(value PHX_URL_PORT) base_url=$(value WNH_BASE_URL) debug_base_url=$(value WNH_DEBUG_BASE_URL) if all_set DEPLOYMENT_ENV PHX_HOST PHX_SCHEME PHX_URL_PORT WNH_BASE_URL WNH_DEBUG_BASE_URL && [[ "$base_url" == "$debug_base_url" ]] && [[ "$base_url" == "$phx_scheme://$phx_host" || "$base_url" == "$phx_scheme://$phx_host:$phx_port" ]] && ! contains_template_marker "$base_url"; then ready "public origin" "deployment=$deployment_env; one canonical Android/web origin" else invalid "public origin" "DEPLOYMENT_ENV/PHX_*/WNH_*_BASE_URL are incomplete or inconsistent" fi if all_set SECRET_KEY_BASE HANDOVER_SECRET RELEASE_COOKIE METRICS_TOKEN; then ready "application secrets" "four required independent values are present" else missing "application secrets" "SECRET_KEY_BASE, HANDOVER_SECRET, RELEASE_COOKIE, METRICS_TOKEN" fi smtp_relay=$(value SMTP_RELAY) email_delivery_provider=$(value EMAIL_DELIVERY_PROVIDER) email_delivery_provider=${email_delivery_provider:-smtp} if [[ "$email_delivery_provider" != "smtp" ]]; then invalid "transactional email" "EMAIL_DELIVERY_PROVIDER must be smtp" elif ! all_set SMTP_RELAY SMTP_PORT SMTP_AUTH SMTP_TLS SMTP_SSL EMAIL_FROM_ADDRESS; then missing "transactional email" "SMTP transport and sender fields" elif [[ "$smtp_relay" == "mailpit" ]]; then local_only "transactional email" "Mailpit captures messages locally; it cannot deliver public email" elif [[ "$(value SMTP_AUTH)" == "always" ]] && ! all_set SMTP_USERNAME SMTP_PASSWORD; then partial "transactional email" "authenticated SMTP requires both username and password" else ready "transactional email" "external SMTP transport is configured" fi if is_set SUPPORT_INBOX_ADDRESS; then ready "support inbox" "operator destination is configured" else missing "support inbox" "SUPPORT_INBOX_ADDRESS" fi if all_empty GOOGLE_OAUTH_CLIENT_ID GOOGLE_OAUTH_CLIENT_SECRET; then missing "Google sign-in" "GOOGLE_OAUTH_CLIENT_ID and GOOGLE_OAUTH_CLIENT_SECRET" elif all_set GOOGLE_OAUTH_CLIENT_ID GOOGLE_OAUTH_CLIENT_SECRET; then ready "Google sign-in" "client ID and secret are both configured" else partial "Google sign-in" "client ID and secret must be configured together" fi if all_empty WEB_PUSH_VAPID_PUBLIC_KEY WEB_PUSH_VAPID_PRIVATE_KEY WEB_PUSH_VAPID_SUBJECT; then missing "browser Web Push" "VAPID public/private keys and subject" elif all_set WEB_PUSH_VAPID_PUBLIC_KEY WEB_PUSH_VAPID_PRIVATE_KEY WEB_PUSH_VAPID_SUBJECT; then case "$(value WEB_PUSH_VAPID_SUBJECT)" in mailto:* | https://*) ready "browser Web Push" "complete VAPID configuration" ;; *) invalid "browser Web Push" "WEB_PUSH_VAPID_SUBJECT must use mailto: or https://" ;; esac else partial "browser Web Push" "all three VAPID values are required together" fi if all_empty WNH_FIREBASE_APPLICATION_ID WNH_FIREBASE_API_KEY \ WNH_FIREBASE_PROJECT_ID WNH_FIREBASE_GCM_SENDER_ID; then missing "Android Firebase client" "four WNH_FIREBASE_* Android client values" elif all_set WNH_FIREBASE_APPLICATION_ID WNH_FIREBASE_API_KEY \ WNH_FIREBASE_PROJECT_ID WNH_FIREBASE_GCM_SENDER_ID; then if firebase_client_values_valid; then ready "Android Firebase client" "complete internally consistent client configuration" else invalid "Android Firebase client" \ "application ID must belong to the numeric configured sender/project number" fi else partial "Android Firebase client" "all four WNH_FIREBASE_* values are required together" fi fcm_file=$(value FCM_SERVICE_ACCOUNT_FILE) fcm_base64=$(value FCM_SERVICE_ACCOUNT_JSON_BASE64) fcm_project_id=$(value FCM_PROJECT_ID) firebase_project_id=$(value WNH_FIREBASE_PROJECT_ID) fcm_credential_project_id= if [[ -z "$fcm_project_id" && -z "$fcm_file" && -z "$fcm_base64" ]]; then missing "Android FCM delivery" "FCM project ID and one service-account source" elif [[ -z "$fcm_project_id" || (-n "$fcm_file" && -n "$fcm_base64") || (-z "$fcm_file" && -z "$fcm_base64") ]]; then partial "Android FCM delivery" "project ID and exactly one credential source are required" elif [[ -n "$fcm_file" ]]; then if [[ "$fcm_file" == /* && -r "$fcm_file" ]] && fcm_credential_project_id=$(fcm_service_account_project_id <"$fcm_file") && [[ "$fcm_credential_project_id" == "$fcm_project_id" ]] && [[ -z "$firebase_project_id" || "$fcm_project_id" == "$firebase_project_id" ]]; then ready "Android FCM delivery" "service account and Android client use the same project" else invalid "Android FCM delivery" \ "credential source and configured Firebase/FCM project IDs are incomplete or inconsistent" fi elif fcm_credential_project_id=$( printf '%s' "$fcm_base64" | base64 --decode 2>/dev/null | fcm_service_account_project_id ) && [[ "$fcm_credential_project_id" == "$fcm_project_id" ]] && [[ -z "$firebase_project_id" || "$fcm_project_id" == "$firebase_project_id" ]]; then ready "Android FCM delivery" "service account and Android client use the same project" else invalid "Android FCM delivery" \ "credential source and configured Firebase/FCM project IDs are incomplete or inconsistent" fi expected_android_package= case "$deployment_env" in development) expected_android_package=org.whoneedhelp.mobile.development ;; test) expected_android_package=org.whoneedhelp.mobile.staging ;; production) expected_android_package=org.whoneedhelp.mobile ;; esac if all_empty ANDROID_APP_LINKS_PACKAGE_NAME \ ANDROID_APP_LINKS_SHA256_CERT_FINGERPRINTS \ ANDROID_PLAY_APP_SIGNING_SHA256_CERT_FINGERPRINTS; then missing "Android App Links" "package name and signing certificate fingerprint" elif all_set ANDROID_APP_LINKS_PACKAGE_NAME ANDROID_APP_LINKS_SHA256_CERT_FINGERPRINTS; then if [[ -z "$expected_android_package" || "$(value ANDROID_APP_LINKS_PACKAGE_NAME)" != "$expected_android_package" ]]; then invalid "Android App Links" "package does not match DEPLOYMENT_ENV=$deployment_env" elif ! valid_sha256_fingerprint_list \ "$(value ANDROID_APP_LINKS_SHA256_CERT_FINGERPRINTS)"; then invalid "Android App Links" "published signing fingerprints are malformed" elif [[ "$deployment_env" != production ]]; then ready "Android App Links" "package and signing fingerprints match this environment" elif ! is_set ANDROID_PLAY_APP_SIGNING_SHA256_CERT_FINGERPRINTS; then missing "Android App Links" "production requires the Play App Signing SHA-256 fingerprint" elif ! valid_sha256_fingerprint_list \ "$(value ANDROID_PLAY_APP_SIGNING_SHA256_CERT_FINGERPRINTS)"; then invalid "Android App Links" "Play App Signing fingerprints are malformed" else published_fingerprints=$(value ANDROID_APP_LINKS_SHA256_CERT_FINGERPRINTS) play_fingerprints=$(value ANDROID_PLAY_APP_SIGNING_SHA256_CERT_FINGERPRINTS) all_play_fingerprints_published=true IFS=',' read -r -a play_fingerprint_list <<<"$play_fingerprints" IFS=',' read -r -a published_fingerprint_list <<<"$published_fingerprints" for play_fingerprint in "${play_fingerprint_list[@]}"; do compact_play=${play_fingerprint//:/} compact_play=${compact_play//[[:space:]]/} play_found=false for published_fingerprint in "${published_fingerprint_list[@]}"; do compact_published=${published_fingerprint//:/} compact_published=${compact_published//[[:space:]]/} if [[ "${compact_play^^}" == "${compact_published^^}" ]]; then play_found=true break fi done if [[ "$play_found" != true ]]; then all_play_fingerprints_published=false break fi done if [[ "$all_play_fingerprints_published" == true ]]; then ready "Android App Links" "published identities include the Play App Signing certificate" else invalid "Android App Links" "Play App Signing fingerprint is absent from the published identities" fi fi else partial "Android App Links" "package and signing fingerprints are incomplete" fi android_signing_alias= android_signing_label= case "$deployment_env" in development) android_signing_alias=WNH_ANDROID_DEVELOPMENT_SIGNING_KEY_ALIAS android_signing_label=development ;; test) android_signing_alias=WNH_ANDROID_STAGING_SIGNING_KEY_ALIAS android_signing_label=staging ;; production) android_signing_alias=WNH_ANDROID_SIGNING_KEY_ALIAS android_signing_label=production ;; esac if [[ -n "$android_signing_alias" ]] && all_set WNH_ANDROID_VERSION_CODE WNH_ANDROID_VERSION_NAME "$android_signing_alias"; then ready "Android release inputs" \ "version and $android_signing_label signing alias are present" else missing "Android release inputs" \ "version code/name and the signing alias for DEPLOYMENT_ENV=$deployment_env" fi printf '\nEnvironment readiness: %d blocking item(s), %d local-only warning(s).\n' \ "$failures" "$warnings" if [[ "$mode" == "--require-release" && ($failures -ne 0 || $warnings -ne 0) ]]; then exit 1 fi