348 lines
12 KiB
Bash
Executable File
348 lines
12 KiB
Bash
Executable File
#!/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
|