Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 76 additions & 11 deletions scripts/setup_storefront_env
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"

ROOT_ENV="${ROOT_DIR}/.env"
ROOT_ENV_LOCAL="${ROOT_DIR}/.env.local"
ANDROID_ENV="${ROOT_DIR}/platforms/android/samples/CheckoutKitAndroidDemo/.env"
SWIFT_DEMO_XCCONFIG="${ROOT_DIR}/platforms/swift/Samples/CheckoutKitSwiftDemo/Storefront.xcconfig"
SWIFT_ACCELERATED_XCCONFIG="${ROOT_DIR}/platforms/swift/Samples/ShopifyAcceleratedCheckoutsApp/Storefront.xcconfig"
Expand Down Expand Up @@ -94,15 +95,37 @@ read_env_value() {
sub(/[[:space:]]*$/, "", candidate)
if (candidate == key) {
value = substr(line, index(line, "=") + 1)
print value
exit
found = 1
}
}
END { if (found) print value }
' "$file")"

strip_outer_quotes "$raw_value"
}

env_keys() {
local file="$1"

[[ -f "$file" ]] || return 0

awk '
/^[[:space:]]*#/ || /^[[:space:]]*\/\// || /^[[:space:]]*$/ { next }
$0 !~ /=/ { next }
{
line = $0
sub(/^[[:space:]]*/, "", line)
candidate = line
sub(/=.*/, "", candidate)
sub(/[[:space:]]*$/, "", candidate)
if (candidate != "" && !(candidate in seen)) {
seen[candidate] = 1
print candidate
}
}
' "$file"
}

env_has_key() {
local key="$1"
local file="$2"
Expand All @@ -127,6 +150,36 @@ env_has_key() {
' "$file"
}

# Value resolution reads .env.local ahead of .env, so an entry there wins.
# Decisions about writing .env deliberately do not use these: .env.local must
# never be baked into .env, or the override would become permanent.
read_root_value() {
local key="$1"

if env_has_key "$key" "$ROOT_ENV_LOCAL"; then
read_env_value "$key" "$ROOT_ENV_LOCAL"
return 0
fi

read_env_value "$key" "$ROOT_ENV"
}

root_has_key() {
local key="$1"

env_has_key "$key" "$ROOT_ENV_LOCAL" || env_has_key "$key" "$ROOT_ENV"
}

report_local_overrides() {
local keys

keys="$(env_keys "$ROOT_ENV_LOCAL" | paste -sd, - | sed -e 's/,/, /g')"
[[ -n "$keys" ]] || return 0

echo ".env.local overrides .env for: ${keys}"
echo "Nothing writes to .env.local, so remove a line there to fall back to .env."
}

is_missing_required_value() {
local value="$1"

Expand Down Expand Up @@ -175,7 +228,7 @@ required_config_value() {
local file_value
local env_value

file_value="$(read_env_value "$key" "$ROOT_ENV")"
file_value="$(read_root_value "$key")"
if ! is_missing_required_value "$file_value"; then
printf '%s' "$file_value"
return 0
Expand All @@ -192,8 +245,8 @@ root_or_source_value() {
shift
local root_value

if env_has_key "$key" "$ROOT_ENV"; then
root_value="$(read_env_value "$key" "$ROOT_ENV")"
if root_has_key "$key"; then
root_value="$(read_root_value "$key")"
if ! is_placeholder_value "$root_value"; then
printf '%s' "$root_value"
return 0
Expand All @@ -208,8 +261,8 @@ root_or_source_nonempty_value() {
shift
local root_value

if env_has_key "$key" "$ROOT_ENV"; then
root_value="$(read_env_value "$key" "$ROOT_ENV")"
if root_has_key "$key"; then
root_value="$(read_root_value "$key")"
if [[ -n "$root_value" ]] && ! is_placeholder_value "$root_value"; then
printf '%s' "$root_value"
return 0
Expand Down Expand Up @@ -408,8 +461,8 @@ load_values() {
"$(read_env_value STOREFRONT_ACCESS_TOKEN "$SWIFT_ACCELERATED_XCCONFIG")")"

API_VERSION_VALUE="$(first_config_value \
"$(read_env_value API_VERSION "$ROOT_ENV")" \
"$(read_env_value STOREFRONT_VERSION "$ROOT_ENV")" \
"$(read_root_value API_VERSION)" \
"$(read_root_value STOREFRONT_VERSION)" \
"$(env_fallback API_VERSION)" \
"$(env_fallback STOREFRONT_VERSION)" \
"$(read_env_value API_VERSION "$ANDROID_ENV")" \
Expand Down Expand Up @@ -703,8 +756,8 @@ ensure_root_env() {
exit 1
fi

if is_missing_required_value "$(read_env_value STOREFRONT_DOMAIN "$ROOT_ENV")" ||
is_missing_required_value "$(read_env_value STOREFRONT_ACCESS_TOKEN "$ROOT_ENV")"; then
if is_missing_required_value "$(read_root_value STOREFRONT_DOMAIN)" ||
is_missing_required_value "$(read_root_value STOREFRONT_ACCESS_TOKEN)"; then
echo "Root .env is missing required storefront configuration." >&2
exit 1
fi
Expand Down Expand Up @@ -738,6 +791,17 @@ ensure_root_env() {
echo "Normalizing root storefront configuration at .env."
fi

# Resolved values carry .env.local overrides, so writing them back would make an
# override permanent. Leave .env alone and say why instead.
if [[ -f "$ROOT_ENV_LOCAL" ]]; then
if [[ "$root_needs_write" == "true" ]]; then
echo "Leaving .env alone because .env.local exists. Remove .env.local to let .env be rewritten."
fi

load_values
return 0
fi

if [[ "$root_needs_write" == "true" ]]; then
collect_missing_values
generate_root_env >"$ROOT_ENV"
Expand Down Expand Up @@ -795,6 +859,7 @@ check_generated_files() {
echo "Sample app storefront configuration is up to date."
}

report_local_overrides
ensure_root_env

if [[ "$mode" == "check" ]]; then
Expand Down
127 changes: 126 additions & 1 deletion scripts/test_setup_storefront_env
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,40 @@ assert_not_contains() {
fi
}

write_canonical_root_env() {
local path="$1"
local domain="$2"

cat >"$path" <<EOF
STOREFRONT_DOMAIN=${domain}
STOREFRONT_ACCESS_TOKEN=synthetic-token
STOREFRONT_MERCHANT_IDENTIFIER=synthetic-merchant
API_VERSION=2026-04
CUSTOMER_ACCOUNT_API_CLIENT_ID=synthetic-client
CUSTOMER_ACCOUNT_API_SHOP_ID=synthetic-shop
CUSTOMER_ACCOUNT_API_VERSION=2026-04
CUSTOM_USER_AGENT=
EMAIL=checkout-kit@example.com
ADDRESS_1=650 King Street
ADDRESS_2=Shopify HQ
CITY=Toronto
COMPANY=Shopify
COUNTRY=CA
FIRST_NAME=Evelyn
LAST_NAME=Hartley
PROVINCE=ON
ZIP=M5V 1M7
PHONE=+14165550100
EOF
}

assert_file_is_unchanged() {
local path="$1"
local expected="$2"

cmp -s "$path" "$expected" || fail "file was rewritten but must stay byte-identical: $path"
}

assert_buyer_identity_keys() {
local path="$1"
local key
Expand All @@ -84,7 +118,10 @@ assert_output_is_sanitized() {
migrated-store.example.myshopify.com \
migrated-token \
migrated-client \
migrated-shop; do
migrated-shop \
overridden-store.example.myshopify.com \
overridden-token \
later-store.example.myshopify.com; do
if grep -Fq "$value" "$output_path"; then
fail "command output included a configured value"
fi
Expand Down Expand Up @@ -378,6 +415,90 @@ EOF
assert_output_is_sanitized "$output"
}

test_duplicate_keys_resolve_last_wins() {
local fixture output android_env
fixture="$(make_fixture)"
output="$fixture/output.log"
android_env="$fixture/platforms/android/samples/CheckoutKitAndroidDemo/.env"

write_canonical_root_env "$fixture/.env" synthetic-store.example.myshopify.com
printf 'STOREFRONT_DOMAIN=%s\n' later-store.example.myshopify.com >>"$fixture/.env"

"$fixture/scripts/setup_storefront_env" --skip-optional-prompts >"$output" 2>&1
assert_output_is_sanitized "$output"

assert_contains "$android_env" "STOREFRONT_DOMAIN=later-store.example.myshopify.com"
assert_not_contains "$android_env" "synthetic-store.example.myshopify.com"
}

test_env_local_overrides_root_env() {
local fixture output android_env root_before local_before
fixture="$(make_fixture)"
output="$fixture/output.log"
android_env="$fixture/platforms/android/samples/CheckoutKitAndroidDemo/.env"
root_before="$fixture/root_before"
local_before="$fixture/local_before"

write_canonical_root_env "$fixture/.env" synthetic-store.example.myshopify.com
cat >"$fixture/.env.local" <<'EOF'
STOREFRONT_DOMAIN=overridden-store.example.myshopify.com
STOREFRONT_ACCESS_TOKEN=overridden-token
EOF
cp "$fixture/.env" "$root_before"
cp "$fixture/.env.local" "$local_before"

"$fixture/scripts/setup_storefront_env" --skip-optional-prompts >"$output" 2>&1
assert_output_is_sanitized "$output"

assert_contains "$android_env" "STOREFRONT_DOMAIN=overridden-store.example.myshopify.com"
assert_contains "$android_env" "STOREFRONT_ACCESS_TOKEN=overridden-token"
assert_contains "$android_env" "EMAIL=checkout-kit@example.com"
assert_file_is_unchanged "$fixture/.env" "$root_before"
assert_file_is_unchanged "$fixture/.env.local" "$local_before"
}

test_env_local_warning_names_only_keys() {
local fixture output
fixture="$(make_fixture)"
output="$fixture/output.log"

write_canonical_root_env "$fixture/.env" synthetic-store.example.myshopify.com
cat >"$fixture/.env.local" <<'EOF'
STOREFRONT_DOMAIN=overridden-store.example.myshopify.com
STOREFRONT_ACCESS_TOKEN=overridden-token
EOF

"$fixture/scripts/setup_storefront_env" --skip-optional-prompts >"$output" 2>&1
assert_output_is_sanitized "$output"

assert_contains "$output" ".env.local"
assert_contains "$output" "STOREFRONT_DOMAIN"
assert_contains "$output" "STOREFRONT_ACCESS_TOKEN"
assert_not_contains "$output" "EMAIL"
}

test_env_local_is_never_baked_into_root_env() {
local fixture output root_before
fixture="$(make_fixture)"
output="$fixture/output.log"
root_before="$fixture/root_before"

cat >"$fixture/.env" <<'EOF'
STOREFRONT_DOMAIN=synthetic-store.example.myshopify.com
STOREFRONT_ACCESS_TOKEN=synthetic-token
EOF
printf 'STOREFRONT_DOMAIN=%s\n' overridden-store.example.myshopify.com >"$fixture/.env.local"
cp "$fixture/.env" "$root_before"

"$fixture/scripts/setup_storefront_env" --skip-optional-prompts >"$output" 2>&1
assert_output_is_sanitized "$output"

assert_file_is_unchanged "$fixture/.env" "$root_before"
assert_contains "$output" "Leaving .env alone"
assert_contains "$fixture/platforms/android/samples/CheckoutKitAndroidDemo/.env" \
"STOREFRONT_DOMAIN=overridden-store.example.myshopify.com"
}

test_sync_and_check
test_required_values_only
test_buyer_address_defaults_match_the_e2e_shop_country
Expand All @@ -387,5 +508,9 @@ test_blank_customer_account_api_version_defaults
test_development_team_follows_env_and_clears
test_development_team_is_added_to_an_existing_env
test_migration_from_platform_config
test_duplicate_keys_resolve_last_wins
test_env_local_overrides_root_env
test_env_local_warning_names_only_keys
test_env_local_is_never_baked_into_root_env

echo "setup_storefront_env synthetic tests passed."
Loading