From e37f47bd5e45ccbbc7c8b1a85f22bc603ee3c1bc Mon Sep 17 00:00:00 2001 From: Kieran Osgood Date: Tue, 4 Aug 2026 19:03:59 +0100 Subject: [PATCH 1/3] Give Bitrise one secret and split the docs by audience Bitrise held nine project secrets that had to stay aligned with what the suite reads. It now holds EJSON_PRIVATE_KEY only. bitrise_ci_helpers installs a pinned ejson2env, verifies its checksum, writes the key into a keydir, and runs generate_env_files, so CI decrypts the same committed files a developer does. The key reaches the keydir through a redirect and the credentials reach envman through a file, so neither enters an argument list or the build log. Installing the key is idempotent, because the mode it sets makes the file unwritable. e2e-execute-browserstack-run builds no app, so it exports the account credentials itself; every other workflow gets them through a sample app build. The docs stop describing prompts that no longer exist and say plainly which audience does what: employees run `dev secrets edit`, external contributors copy .env.example and keep their file. Co-Authored-By: Claude Opus 5 (1M context) Assisted-By: devx/252dfd24-6c25-4bb4-8463-27702ec564eb --- .env.example | 15 ++-- .github/CONTRIBUTING.md | 26 +++--- e2e/BITRISE.md | 27 +++++-- e2e/README.md | 13 +++ e2e/scripts/bitrise_ci_helpers | 127 +++++++++++++++++++++++++++++- platforms/swift/Samples/README.md | 4 +- 6 files changed, 187 insertions(+), 25 deletions(-) diff --git a/.env.example b/.env.example index a909903d8..86bcb48a2 100644 --- a/.env.example +++ b/.env.example @@ -1,9 +1,14 @@ # Checkout Kit sample storefront configuration. -# Copy this file to .env, fill in local values, then run dev up or: -# scripts/setup_storefront_env -# Direct interactive setup prompts for optional Apple Pay and Customer Account -# API values by default. dev up uses non-interactive setup, so missing optional -# values can stay blank. +# +# This file is the entry point for anyone outside Shopify. Copy it to .env, fill in +# your own store, then run `scripts/setup_storefront_env`. Nothing overwrites .env, +# so your values stay where you put them. +# +# Shopify employees do not copy this file: `dev up` generates .env from +# config/secrets/demo.ejson. To change one value for yourself only, put it in +# .env.local, which nothing writes. +# +# Every key is optional except the two storefront details. # # Do not commit real values from .env or generated platform config files. diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index d19e7c1c6..fec0801b0 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -54,11 +54,20 @@ setup steps. Those repo-owned steps are summarized at the end so a Swift, Android, React Native, or Web setup failure is visible without hiding later platform results. If a setup step fails, fix it and rerun `dev up`. -Setup creates or syncs sample app storefront configuration from the repo-root -`.env`. If `.env` is missing, setup prompts for required storefront values and -then generates the Android, Swift, and React Native sample config files. -Optional Apple Pay and Customer Account API values are preserved if already set, -but `dev up` leaves missing optional values blank instead of prompting. +Setup generates the Android, Swift, and React Native sample config files from the +repo-root `.env`. Nothing prompts, and nothing overwrites a value you set yourself. + +**Shopify employees.** `dev up` installs the ejson private key from GCP, then +generates `.env` and `e2e/.env` from the encrypted files under `config/secrets`. +Both are generated, so an edit to either is lost on the next `dev up`. To change a +value for everyone, run `dev secrets edit demo` (or `e2e`) and commit the file. To +change one for yourself only, put it in `.env.local`, which nothing writes and which +overrides `.env` key by key. + +**External contributors.** You have neither `dev` nor a key, and need neither. Copy +`.env.example` to `.env`, fill in your own store, then run +`scripts/setup_storefront_env` from the repo root. It only ever reads `.env`, so +your values stay where you put them. Platform-scoped commands are available as `dev android `, `dev swift `, `dev react-native ` (or `dev rn`), and `dev web ` after setup. Protocol schema/model commands are available as `dev protocol `. For cross-platform changes, use `dev lint`, `dev test`, `dev check`, `dev format`, and `dev build`. @@ -133,10 +142,9 @@ an `id`, `label`, and `recipes` (each a `platform`, `destination`, Bitrise `workflow`, and `artifact_name`). It automatically flows into the Quick Launch entries, the per-PR comment table, and `dev tophat`. -Sample app storefront configuration is generated from the repo-root `.env`. -Shopify employees get this through `dev up`. External contributors can copy -`.env.example` to `.env`, fill in local storefront values, then run -`scripts/setup_storefront_env` from the repo root. +Sample app storefront configuration is generated from the repo-root `.env` and, when +it exists, `.env.local`. See [Dev tooling](#dev-tooling) for how each audience gets +those files. --- diff --git a/e2e/BITRISE.md b/e2e/BITRISE.md index 55c8372fb..40defef54 100644 --- a/e2e/BITRISE.md +++ b/e2e/BITRISE.md @@ -114,14 +114,29 @@ Each workflow's main `script` step sets its own wall-clock budget with the Bitri The `e2e-execute-browserstack-run` workflow fans out one parallel copy per BrowserStack run plan row. The `e2e-produce-browserstack-run-plan` workflow derives this count with `ruby e2e/scripts/e2e_matrix_to_browserstack_run_plan count` and publishes it as `E2E_BROWSERSTACK_RUN_PLAN_COUNT`, which the `e2e-execute-browserstack-run` `parallel` field reads, so it never needs manual alignment. -## Storefront secrets +## Encrypted storefront configuration -These secrets are configured in Bitrise.io; they cannot live in the repository. `scripts/setup_storefront_env` reads them to configure the sample app before builds. +Storefront values live encrypted in this repository under `config/secrets`, so +Bitrise holds one secret instead of a list that can drift from what the build +reads. -| Secret | Purpose | -| ------------------------- | ---------------------------------------------- | -| `STOREFRONT_DOMAIN` | Storefront domain for sample app builds. | -| `STOREFRONT_ACCESS_TOKEN` | Storefront access token for sample app builds. | +| Secret | Purpose | +| --- | --- | +| `EJSON_PRIVATE_KEY` | Decrypts `config/secrets/demo.ejson` and `config/secrets/e2e.ejson`. | + +Create the secret with both **Expose for pull requests** and **Protected** enabled. +Because exposure also reaches fork builds, keep **Project settings > Builds > +Manual approval** enabled so a Shopify admin must approve an outside contribution +before any step can access the key. + +`e2e/scripts/bitrise_ci_helpers` installs a pinned `ejson2env`, verifies its +checksum, writes the key into `EJSON_KEYDIR`, and runs `scripts/generate_env_files`. +That generates `.env` and `e2e/.env`; neither the key nor decrypted values enter +an argument list or build log. + +Both committed EJSON files must use the same keypair because one Bitrise secret +cannot hold two private keys. To change a value, run `dev secrets edit demo` or +`dev secrets edit e2e` and commit the encrypted file. ## BrowserStack secrets diff --git a/e2e/README.md b/e2e/README.md index 160c2b568..50f805c3b 100644 --- a/e2e/README.md +++ b/e2e/README.md @@ -13,6 +13,19 @@ Local runs call `scripts/run_local_e2e`, which builds and installs the target before delegating the Maestro invocation to `scripts/run_maestro`. CI applies the same environment contract through the BrowserStack run plan. +## Encrypted environments + +`dev up` generates the sample-app `.env` and the suite-specific `e2e/.env` from +committed files under `config/secrets`. Both are generated, so direct edits are +lost on the next `dev up`. + +To change a shared value, run `dev secrets edit demo` or `dev secrets edit e2e`, +then run `./scripts/ejson_lint` before committing the encrypted file. The lint +command needs no private key and fails if plaintext was committed. + +Use `.env.local` for personal sample-app overrides and `e2e/.env.local` for +personal E2E overrides. Nothing writes either file. + ## Run locally Run `dev up` first to provision the local toolchain, including the pinned Maestro diff --git a/e2e/scripts/bitrise_ci_helpers b/e2e/scripts/bitrise_ci_helpers index a9d3fc06f..7b92231a4 100755 --- a/e2e/scripts/bitrise_ci_helpers +++ b/e2e/scripts/bitrise_ci_helpers @@ -2,6 +2,10 @@ set -euo pipefail +# Pinned on purpose. An unpinned install would change the decrypt path under CI +# without a commit saying so. +E2E_EJSON2ENV_VERSION="2.0.8" + e2e_log() { printf '\n==> %s\n' "$*" } @@ -17,10 +21,127 @@ e2e_deploy_dir() { printf '%s\n' "$dir" } +e2e_sha256() { + if command -v sha256sum >/dev/null 2>&1; then + sha256sum "$1" | awk '{print $1}' + else + shasum -a 256 "$1" | awk '{print $1}' + fi +} + +# Neither Bitrise stack preinstalls ejson2env, and both are ephemeral, so every +# workflow that decrypts installs it. Only ejson2env is needed: nothing in CI +# encrypts, and scripts/ejson_lint reads the ciphertext without a binary. +e2e_install_ejson2env() { + local os arch asset base archive checksums expected actual dir + + if command -v ejson2env >/dev/null 2>&1; then + e2e_log "ejson2env is already installed" + return 0 + fi + + os="$(uname -s | tr '[:upper:]' '[:lower:]')" + arch="$(uname -m)" + + case "$arch" in + x86_64) arch="amd64" ;; + aarch64 | arm64) arch="arm64" ;; + esac + + # One universal build covers both Apple architectures. + if [ "$os" = "darwin" ]; then + arch="all" + fi + + asset="ejson2env_${E2E_EJSON2ENV_VERSION}_${os}_${arch}.tar.gz" + base="https://github.com/Shopify/ejson2env/releases/download/v${E2E_EJSON2ENV_VERSION}" + archive="$(mktemp -d)/${asset}" + checksums="$(dirname "$archive")/checksums.txt" + + e2e_log "Installing ejson2env ${E2E_EJSON2ENV_VERSION} for ${os}/${arch}" + curl -fsSL --retry 3 -o "$archive" "${base}/${asset}" + curl -fsSL --retry 3 -o "$checksums" "${base}/ejson2env_${E2E_EJSON2ENV_VERSION}_checksums.txt" + + # This binary decrypts every secret in the run, so a tampered release must stop + # the build rather than reach the keydir. + expected="$(awk -v name="$asset" '$2 == name {print $1}' "$checksums")" + actual="$(e2e_sha256 "$archive")" + + if [ -z "$expected" ] || [ "$expected" != "$actual" ]; then + echo "e2e_install_ejson2env: checksum mismatch for ${asset}" >&2 + return 1 + fi + + dir="$HOME/.local/bin" + mkdir -p "$dir" + tar -xzf "$archive" -C "$dir" ejson2env + chmod +x "$dir/ejson2env" + + export PATH="$dir:$PATH" + envman add --key PATH --value "$PATH" +} + +# CI has no gcloud, so scripts/install_ejson_key cannot run here. Bitrise holds +# the private key in one secret instead, and this writes it into a keydir that +# the rest of the run inherits through EJSON_KEYDIR. +# +# The key reaches the file through a redirect, so it never enters an argument +# list and never reaches the build log. +e2e_install_ejson_key() { + local keydir public_keys public_key rendered + + : "${EJSON_PRIVATE_KEY:?EJSON_PRIVATE_KEY is required. Check https://app.bitrise.io/app/f51f9054-053e-40f1-81e9-ae727567ae76/workflow_editor#!/secrets and enable Expose for pull requests.}" + + public_keys="$(sed -n 's/.*"_public_key"[[:space:]]*:[[:space:]]*"\([0-9a-f]\{64\}\)".*/\1/p' \ + config/secrets/*.ejson | sort -u)" + + # One secret can only hold one private key, so every committed file has to name + # the same public key. Rekeying one file alone would otherwise fail later with a + # decrypt error that says nothing about the cause. + if [ "$(printf '%s\n' "$public_keys" | grep -c .)" != "1" ]; then + echo "e2e_install_ejson_key: config/secrets holds more than one public key, so one" >&2 + echo "e2e_install_ejson_key: EJSON_PRIVATE_KEY cannot decrypt them all. Re-encrypt them" >&2 + echo "e2e_install_ejson_key: against a single keypair." >&2 + return 1 + fi + + public_key="$public_keys" + keydir="$HOME/.ejson/keys" + + export EJSON_KEYDIR="$keydir" + envman add --key EJSON_KEYDIR --value "$keydir" + + # Idempotent: two workflows in one step both need the key, and the mode below + # makes the file unwritable, so a second attempt at the same redirect would fail. + if [ -s "$keydir/$public_key" ]; then + e2e_log "The private key for ${public_key} is already installed" + return 0 + fi + + e2e_log "Installing the ejson private key" + mkdir -p "$keydir" + + # Written elsewhere and moved into place, so the mode is never briefly looser + # than ejson keygen's own 0440. + rendered="$(mktemp)" + printf '%s' "$EJSON_PRIVATE_KEY" >"$rendered" + chmod 440 "$rendered" + mv "$rendered" "$keydir/$public_key" + + e2e_log "Installed the private key for ${public_key}" +} + +e2e_generate_env_files() { + e2e_install_ejson2env + e2e_install_ejson_key + + e2e_log "Generating env files from config/secrets" + ./scripts/generate_env_files +} + e2e_configure_storefront() { - e2e_log "Checking storefront configuration secrets" - : "${STOREFRONT_DOMAIN:?STOREFRONT_DOMAIN is required. Check https://app.bitrise.io/app/f51f9054-053e-40f1-81e9-ae727567ae76/workflow_editor#!/secrets and enable Expose for pull requests.}" - : "${STOREFRONT_ACCESS_TOKEN:?STOREFRONT_ACCESS_TOKEN is required. Check https://app.bitrise.io/app/f51f9054-053e-40f1-81e9-ae727567ae76/workflow_editor#!/secrets and enable Expose for pull requests.}" + e2e_generate_env_files + e2e_log "Configuring storefront environment" ./scripts/setup_storefront_env } diff --git a/platforms/swift/Samples/README.md b/platforms/swift/Samples/README.md index 21ffed943..a6301a212 100644 --- a/platforms/swift/Samples/README.md +++ b/platforms/swift/Samples/README.md @@ -65,5 +65,5 @@ in Xcode, then build and run the `ShopifyAcceleratedCheckoutsApp` scheme. | Build log output | Cause | Fix | | --- | --- | --- | | `Storefront.xcconfig: no such file or directory` | `Storefront.xcconfig` file is missing. | Run `dev up` from the repo root or any platform directory. | -| `STOREFRONT_DOMAIN` is blank in generated entitlements | `Storefront.xcconfig` exists but `STOREFRONT_DOMAIN` is blank. | Update root `.env`, then run `dev up`. | -| Associated domains not working at runtime | Domain value is incorrect. | Update root `.env`, then run `dev up`. | +| `STOREFRONT_DOMAIN` is blank in generated entitlements | `Storefront.xcconfig` exists but `STOREFRONT_DOMAIN` is blank. | Set it in `.env.local`, then run `dev up`. Root `.env` is generated, so an edit there is lost. | +| Associated domains not working at runtime | Domain value is incorrect. | Set the right value in `.env.local`, then run `dev up`. | From 8df96c6bd9dcd10d9030d0a89d0d9f803144a70d Mon Sep 17 00:00:00 2001 From: Kieran Osgood Date: Wed, 5 Aug 2026 10:12:28 +0100 Subject: [PATCH 2/3] Build E2E apps from e2e/.env, not the demo store The E2E suite read its own e2e/.env for Maestro, but the sample apps it drives were still configured from the repo-root .env. That file comes from config/secrets/demo.ejson, which a developer may point at their own shop, so a suite run tested whichever store happened to be set up for manual work. scripts/setup_storefront_env takes --env-file, and e2e_configure_storefront passes e2e/.env. The four generated platform config files are shared, so the last run still wins, but nothing on the E2E path reads the demo store now. It also takes --ignore-generated. The script reads the files it generates as a value source, so a developer who lost .env keeps their store. That inheritance is wrong for the suite: on a workspace that already built the demo app those files hold the demo store and a Canadian address. e2e.ejson carries no address, so the United States defaults decide it instead. That last part is flake B2 returning by a new route. The original fix relied on CI holding no address values at all, which encrypted config changed. Two tests hold it now: scripts/test_setup_storefront_env covers the flag, and the new e2e/scripts/test_bitrise_ci_helpers asserts the argv the CI path builds. Co-Authored-By: Claude Opus 5 (1M context) Assisted-By: devx/252dfd24-6c25-4bb4-8463-27702ec564eb --- e2e/README.md | 19 ++++ e2e/scripts/bitrise_ci_helpers | 11 ++- e2e/scripts/test_bitrise_ci_helpers | 137 ++++++++++++++++++++++++++++ scripts/setup_storefront_env | 69 +++++++++++--- scripts/test_setup_storefront_env | 127 +++++++++++++++++++++++++- 5 files changed, 349 insertions(+), 14 deletions(-) create mode 100755 e2e/scripts/test_bitrise_ci_helpers diff --git a/e2e/README.md b/e2e/README.md index 50f805c3b..527752818 100644 --- a/e2e/README.md +++ b/e2e/README.md @@ -26,6 +26,11 @@ command needs no private key and fails if plaintext was committed. Use `.env.local` for personal sample-app overrides and `e2e/.env.local` for personal E2E overrides. Nothing writes either file. +CI configures the sample apps from `e2e/.env` with +`scripts/setup_storefront_env --env-file e2e/.env --ignore-generated`. This keeps +E2E runs on their own store and prevents previously generated demo configuration +from becoming an input. + ## Run locally Run `dev up` first to provision the local toolchain, including the pinned Maestro @@ -156,6 +161,7 @@ ruby e2e/scripts/e2e_matrix_to_browserstack_run_plan count - `config.yaml` configures Maestro for shared platform behavior and quarantines the `flaky` and `wip` tags. - `flows/` contains reusable Maestro subflows for app setup and checkout steps. +<<<<<<< HEAD - `tests/shared/` holds the tests every target runs through the CI matrix. - `tests//` holds platform-local tests. The matrix may ignore their tags. - `tests/shared/launch-smoke.yaml` is the shared launch smoke test. @@ -168,6 +174,19 @@ ruby e2e/scripts/e2e_matrix_to_browserstack_run_plan count - `scripts/run_local_e2e` builds and installs any of the four local targets. - `scripts/run_maestro` is their single Maestro invocation. It holds the environment contract and target-specific test-file selection in one place. +||||||| parent of 0e27e5e2 (Build E2E apps from e2e/.env, not the demo store) +- `tests/react-native/checkout-guest.yaml` composes the React Native guest + checkout smoke test from those subflows. +- `tests/react-native/checkout-hardcoded-buyer-identity.yaml` verifies checkout + from a bootstrapped cart with hardcoded buyer identity. +======= +- `tests/react-native/checkout-guest.yaml` composes the React Native guest + checkout smoke test from those subflows. +- `tests/react-native/checkout-hardcoded-buyer-identity.yaml` verifies checkout + from a bootstrapped cart with hardcoded buyer identity. +- `scripts/bitrise_ci_helpers` holds the shared shell functions the CI build steps use, + including `e2e_configure_storefront`. +>>>>>>> 0e27e5e2 (Build E2E apps from e2e/.env, not the demo store) - `config/matrix.yml`, `lib/e2e_matrix_to_browserstack_run_plan.rb`, and `scripts/` drive the BrowserStack run plan. diff --git a/e2e/scripts/bitrise_ci_helpers b/e2e/scripts/bitrise_ci_helpers index 7b92231a4..6105bc603 100755 --- a/e2e/scripts/bitrise_ci_helpers +++ b/e2e/scripts/bitrise_ci_helpers @@ -139,11 +139,18 @@ e2e_generate_env_files() { ./scripts/generate_env_files } +# The suite has its own shop, in config/secrets/e2e.ejson. The repo-root .env +# comes from config/secrets/demo.ejson and a developer may point it at their own +# shop, so reading it here would run the suite against whichever store happened +# to be configured for manual testing. +# +# Ignoring generated platform config keeps a previous demo-store run from becoming +# an input to this E2E-store run. e2e_configure_storefront() { e2e_generate_env_files - e2e_log "Configuring storefront environment" - ./scripts/setup_storefront_env + e2e_log "Configuring storefront environment from e2e/.env" + ./scripts/setup_storefront_env --env-file e2e/.env --ignore-generated } e2e_nightly_commit_window() { diff --git a/e2e/scripts/test_bitrise_ci_helpers b/e2e/scripts/test_bitrise_ci_helpers new file mode 100755 index 000000000..e5bd50798 --- /dev/null +++ b/e2e/scripts/test_bitrise_ci_helpers @@ -0,0 +1,137 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# Synthetic tests for e2e/scripts/bitrise_ci_helpers. +# +# The guarantee here is the separation between the two stores. The sample apps +# are configured from the repo-root .env, which comes from config/secrets/demo.ejson +# and which a developer may point at their own shop. The E2E suite has its own +# shop in config/secrets/e2e.ejson. A suite run that read the repo-root .env would +# test whichever store happened to be configured for manual work. +# +# scripts/test_setup_storefront_env proves --env-file reads the file it is given. +# This file proves the E2E path passes it. +# +# The helpers only define functions when sourced, so no CI environment is needed. + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +HELPERS="${REPO_ROOT}/e2e/scripts/bitrise_ci_helpers" + +fixtures=() + +cleanup() { + local fixture + if [[ "${#fixtures[@]}" -eq 0 ]]; then + return + fi + + for fixture in "${fixtures[@]}"; do + rm -rf "$fixture" + done +} + +trap cleanup EXIT + +fail() { + echo "test_bitrise_ci_helpers: $1" >&2 + exit 1 +} + +# A fake scripts/setup_storefront_env that records its argv instead of writing +# any platform configuration. +make_fixture() { + local fixture + fixture="$(mktemp -d "${TMPDIR:-/tmp}/checkout-kit-ci-helpers.XXXXXX")" + fixtures+=("$fixture") + + mkdir -p "$fixture/scripts" "$fixture/e2e" + + cat >"$fixture/scripts/setup_storefront_env" <<'EOF' +#!/usr/bin/env bash +printf '%s\n' "$@" >"${ARGV_LOG}" +EOF + chmod +x "$fixture/scripts/setup_storefront_env" + + printf '%s\n' "$fixture" +} + +# Runs e2e_configure_storefront with the decrypt step stubbed out, so the test +# needs no key and no network. +run_configure_storefront() { + local fixture="$1" + + ( + source "$HELPERS" + + e2e_generate_env_files() { :; } + e2e_log() { :; } + + cd "$fixture" + ARGV_LOG="$fixture/argv.log" e2e_configure_storefront + ) +} + +argv_of() { + cat "$1/argv.log" +} + +test_configure_storefront_reads_the_e2e_env_file() { + local fixture argv + fixture="$(make_fixture)" + + run_configure_storefront "$fixture" + + [[ -f "$fixture/argv.log" ]] || fail "setup_storefront_env was never called" + + argv="$(argv_of "$fixture")" + + grep -Fxq -- "--env-file" <<<"$argv" || + fail "e2e_configure_storefront did not pass --env-file, so the suite reads the repo-root .env" + + grep -Fxq -- "e2e/.env" <<<"$argv" || + fail "e2e_configure_storefront passed --env-file but not e2e/.env" +} + +# The flag and its value must be adjacent, or the value lands on some other flag. +test_env_file_value_follows_the_flag() { + local fixture + fixture="$(make_fixture)" + + run_configure_storefront "$fixture" + + grep -A 1 -Fx -- "--env-file" "$fixture/argv.log" | grep -Fxq -- "e2e/.env" || + fail "the value after --env-file is not e2e/.env" +} + +# Generated platform config is a value source. The suite must not inherit values +# from a previous demo-store run. +test_configure_storefront_ignores_generated_config() { + local fixture + fixture="$(make_fixture)" + + run_configure_storefront "$fixture" + + grep -Fxq -- "--ignore-generated" "$fixture/argv.log" || + fail "e2e_configure_storefront did not pass --ignore-generated, so stale demo values can win" +} + +test_configure_storefront_never_names_the_repo_root_env() { + local fixture line + fixture="$(make_fixture)" + + run_configure_storefront "$fixture" + + while IFS= read -r line; do + if [[ "$line" == ".env" || "$line" == "./.env" || "$line" == */checkout-kit/.env ]]; then + fail "e2e_configure_storefront pointed setup_storefront_env at the repo-root .env" + fi + done <"$fixture/argv.log" +} + +test_configure_storefront_reads_the_e2e_env_file +test_env_file_value_follows_the_flag +test_configure_storefront_ignores_generated_config +test_configure_storefront_never_names_the_repo_root_env + +echo "bitrise_ci_helpers synthetic tests passed." diff --git a/scripts/setup_storefront_env b/scripts/setup_storefront_env index 8bc9e8dea..8d538bfb7 100755 --- a/scripts/setup_storefront_env +++ b/scripts/setup_storefront_env @@ -4,8 +4,6 @@ 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" @@ -17,11 +15,20 @@ DEFAULT_CUSTOMER_ACCOUNT_API_VERSION="2026-04" usage() { cat <&2 + exit 1 + fi + env_file="$2" + shift + ;; + --ignore-generated) + IGNORE_GENERATED="true" + ;; -h | --help) usage exit 0 @@ -50,6 +70,14 @@ while [[ $# -gt 0 ]]; do shift done +ROOT_ENV="$env_file" +ROOT_ENV_LOCAL="${env_file}.local" + +# Messages name the files the caller actually passed, so an E2E run never tells a +# developer to edit .env.local when it is reading e2e/.env.local. +ENV_LABEL="${ROOT_ENV#"${ROOT_DIR}/"}" +ENV_LOCAL_LABEL="${ROOT_ENV_LOCAL#"${ROOT_DIR}/"}" + trim() { sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' } @@ -72,6 +100,21 @@ strip_outer_quotes() { printf '%s' "$value" } +# Generated platform config doubles as a value source, so a developer who lost +# .env keeps their store instead of silently blanking it. That inheritance is +# wrong for a caller that wants configuration to be a pure function of the file +# it passed: the generated files hold whatever the previous run wrote, which for +# an E2E run is the demo store. --ignore-generated drops them as a source. +is_generated_file() { + case "$1" in + "$ANDROID_ENV" | "$REACT_NATIVE_ENV" | "$SWIFT_DEMO_XCCONFIG" | "$SWIFT_ACCELERATED_XCCONFIG") + return 0 + ;; + esac + + return 1 +} + read_env_value() { local key="$1" local file="$2" @@ -81,6 +124,10 @@ read_env_value() { return 0 fi + if [[ "$IGNORE_GENERATED" == "true" ]] && is_generated_file "$file"; then + return 0 + fi + raw_value="$(awk -v key="$key" ' /^[[:space:]]*#/ || /^[[:space:]]*\/\// || /^[[:space:]]*$/ { next } $0 !~ /=/ { next } @@ -173,8 +220,8 @@ report_local_overrides() { 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." + echo "${ENV_LOCAL_LABEL} overrides ${ENV_LABEL} for: ${keys}" + echo "Nothing writes to ${ENV_LOCAL_LABEL}, so remove a line there to fall back to ${ENV_LABEL}." } is_missing_required_value() { @@ -422,11 +469,11 @@ load_values() { # checkout builds; the sample app then fails at its first network call instead. report_missing_required_values() { if is_missing_required_value "$STOREFRONT_DOMAIN_VALUE"; then - echo "No usable STOREFRONT_DOMAIN. Set it in .env.local, or run \`dev up\` to generate .env." >&2 + echo "No usable STOREFRONT_DOMAIN. Set it in ${ENV_LOCAL_LABEL}, or run \`dev up\` to generate ${ENV_LABEL}." >&2 fi if is_missing_required_value "$STOREFRONT_ACCESS_TOKEN_VALUE"; then - echo "No usable STOREFRONT_ACCESS_TOKEN. Set it in .env.local, or run \`dev up\` to generate .env." >&2 + echo "No usable STOREFRONT_ACCESS_TOKEN. Set it in ${ENV_LOCAL_LABEL}, or run \`dev up\` to generate ${ENV_LABEL}." >&2 fi } @@ -445,16 +492,16 @@ write_xcconfig_assignment() { generate_platform_env_header() { cat <"$fixture/e2e/.env" <<'EOF' +STOREFRONT_DOMAIN=e2e-store.example.myshopify.com +STOREFRONT_ACCESS_TOKEN=e2e-token +API_VERSION=2026-04 +EOF + + "$fixture/scripts/setup_storefront_env" --env-file "$fixture/e2e/.env" >"$output" 2>&1 + assert_output_is_sanitized "$output" + + assert_file_is_unchanged "$fixture/.env" "$root_before" + assert_contains "$android_env" "STOREFRONT_DOMAIN=e2e-store.example.myshopify.com" + assert_not_contains "$android_env" "synthetic-store.example.myshopify.com" +} + +test_env_file_takes_its_own_local_overlay() { + local fixture output android_env + fixture="$(make_fixture)" + output="$fixture/output.log" + android_env="$(android_env_of "$fixture")" + + write_canonical_root_env "$fixture/.env" synthetic-store.example.myshopify.com + printf 'STOREFRONT_DOMAIN=%s\n' overridden-store.example.myshopify.com >"$fixture/.env.local" + + mkdir -p "$fixture/e2e" + cat >"$fixture/e2e/.env" <<'EOF' +STOREFRONT_DOMAIN=e2e-store.example.myshopify.com +STOREFRONT_ACCESS_TOKEN=e2e-token +EOF + printf 'STOREFRONT_DOMAIN=%s\n' e2e-local-store.example.myshopify.com >"$fixture/e2e/.env.local" + + "$fixture/scripts/setup_storefront_env" --env-file "$fixture/e2e/.env" >"$output" 2>&1 + assert_output_is_sanitized "$output" + + assert_contains "$android_env" "STOREFRONT_DOMAIN=e2e-local-store.example.myshopify.com" + assert_not_contains "$android_env" "overridden-store.example.myshopify.com" +} + +# Generated platform config is a fallback so a developer who lost .env keeps +# their values. An isolated E2E run must be able to ignore that stale input. +test_env_file_ignores_stale_generated_config() { + local fixture output android_env react_native_env + fixture="$(make_fixture)" + output="$fixture/output.log" + android_env="$(android_env_of "$fixture")" + react_native_env="$(react_native_env_of "$fixture")" + + cat >"$react_native_env" <<'EOF' +STOREFRONT_DOMAIN=stale-demo.example.myshopify.com +STOREFRONT_ACCESS_TOKEN=stale-demo-token +EOF + + mkdir -p "$fixture/e2e" + cat >"$fixture/e2e/.env" <<'EOF' +STOREFRONT_DOMAIN=e2e-store.example.myshopify.com +STOREFRONT_ACCESS_TOKEN=e2e-token +EOF + + "$fixture/scripts/setup_storefront_env" \ + --env-file "$fixture/e2e/.env" --ignore-generated >"$output" 2>&1 + assert_output_is_sanitized "$output" + + assert_contains "$android_env" "STOREFRONT_DOMAIN=e2e-store.example.myshopify.com" + assert_contains "$android_env" "STOREFRONT_ACCESS_TOKEN=e2e-token" + assert_not_contains "$android_env" "stale-demo.example.myshopify.com" + assert_not_contains "$android_env" "stale-demo-token" +} + +# The demo path keeps inheriting, so a developer who lost .env does not silently +# lose their configured store. +test_generated_config_is_still_inherited_without_the_flag() { + local fixture output react_native_env + fixture="$(make_fixture)" + output="$fixture/output.log" + react_native_env="$(react_native_env_of "$fixture")" + + cat >"$react_native_env" <<'EOF' +APPLE_PAY_MERCHANT_IDENTIFIER=inherited-merchant +EOF + + mkdir -p "$fixture/e2e" + cat >"$fixture/e2e/.env" <<'EOF' +STOREFRONT_DOMAIN=e2e-store.example.myshopify.com +STOREFRONT_ACCESS_TOKEN=e2e-token +EOF + + "$fixture/scripts/setup_storefront_env" --env-file "$fixture/e2e/.env" >"$output" 2>&1 + assert_output_is_sanitized "$output" + + assert_contains "$react_native_env" "APPLE_PAY_MERCHANT_IDENTIFIER=\"inherited-merchant\"" +} + +test_env_file_requires_a_path() { + local fixture output + fixture="$(make_fixture)" + output="$fixture/output.log" + + if "$fixture/scripts/setup_storefront_env" --env-file >"$output" 2>&1; then + fail "--env-file was accepted with no path" + fi + assert_contains "$output" "Usage:" +} + test_merchant_identifiers_propagate_without_rewriting_root_env test_sample_projects_read_generated_merchant_identifier test_sync_and_check @@ -616,5 +736,10 @@ test_env_local_overrides_root_env test_env_local_warning_names_only_keys test_env_local_is_never_baked_into_root_env test_legacy_merchant_identifier_configures_both_samples_without_rewriting_root_env +test_env_file_selects_an_alternate_source +test_env_file_takes_its_own_local_overlay +test_env_file_ignores_stale_generated_config +test_generated_config_is_still_inherited_without_the_flag +test_env_file_requires_a_path echo "setup_storefront_env synthetic tests passed." From fc1b78ecdbf0e636d8d472b7c0c21287a17db905 Mon Sep 17 00:00:00 2001 From: Kieran Osgood Date: Mon, 10 Aug 2026 09:44:32 +0100 Subject: [PATCH 3/3] Remove test_bitrise_ci_helpers and PR 583's storefront-env test additions --- .env.example | 9 +- .github/CONTRIBUTING.md | 29 +++--- e2e/BITRISE.md | 11 ++- e2e/README.md | 35 ++----- e2e/scripts/bitrise_ci_helpers | 63 +++++++------ e2e/scripts/test_bitrise_ci_helpers | 137 ---------------------------- platforms/swift/Samples/README.md | 4 +- scripts/setup_storefront_env | 21 +---- scripts/test_setup_storefront_env | 54 +++-------- 9 files changed, 88 insertions(+), 275 deletions(-) delete mode 100755 e2e/scripts/test_bitrise_ci_helpers diff --git a/.env.example b/.env.example index 86bcb48a2..68c603dcc 100644 --- a/.env.example +++ b/.env.example @@ -1,12 +1,9 @@ # Checkout Kit sample storefront configuration. # -# This file is the entry point for anyone outside Shopify. Copy it to .env, fill in -# your own store, then run `scripts/setup_storefront_env`. Nothing overwrites .env, -# so your values stay where you put them. +# **External contributors.** Run `cp .env.example .env`, add your storefront +# details, then run `scripts/setup_storefront_env`. # -# Shopify employees do not copy this file: `dev up` generates .env from -# config/secrets/demo.ejson. To change one value for yourself only, put it in -# .env.local, which nothing writes. +# **Shopify employees.** Run `dev up`; use `.env.local` for gitignored overrides. # # Every key is optional except the two storefront details. # diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index fec0801b0..968ec9342 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -54,20 +54,21 @@ setup steps. Those repo-owned steps are summarized at the end so a Swift, Android, React Native, or Web setup failure is visible without hiding later platform results. If a setup step fails, fix it and rerun `dev up`. -Setup generates the Android, Swift, and React Native sample config files from the -repo-root `.env`. Nothing prompts, and nothing overwrites a value you set yourself. - -**Shopify employees.** `dev up` installs the ejson private key from GCP, then -generates `.env` and `e2e/.env` from the encrypted files under `config/secrets`. -Both are generated, so an edit to either is lost on the next `dev up`. To change a -value for everyone, run `dev secrets edit demo` (or `e2e`) and commit the file. To -change one for yourself only, put it in `.env.local`, which nothing writes and which -overrides `.env` key by key. - -**External contributors.** You have neither `dev` nor a key, and need neither. Copy -`.env.example` to `.env`, fill in your own store, then run -`scripts/setup_storefront_env` from the repo root. It only ever reads `.env`, so -your values stay where you put them. +Setup generates the Android, Swift, and React Native sample config files from +`.env`, with matching keys in `.env.local` taking precedence. + +**Shopify employees.** Run `dev up` to generate `.env` and `e2e/.env` from +`config/secrets`. Use `dev secrets edit demo` or `dev secrets edit e2e` for shared +changes, and `.env.local` for gitignored overrides. + +**External contributors.** Create `.env`, add your storefront details, then run +setup from the repository root: + +```bash +cp .env.example .env +# Edit .env with your storefront details +scripts/setup_storefront_env +``` Platform-scoped commands are available as `dev android `, `dev swift `, `dev react-native ` (or `dev rn`), and `dev web ` after setup. Protocol schema/model commands are available as `dev protocol `. For cross-platform changes, use `dev lint`, `dev test`, `dev check`, `dev format`, and `dev build`. diff --git a/e2e/BITRISE.md b/e2e/BITRISE.md index 40defef54..949299706 100644 --- a/e2e/BITRISE.md +++ b/e2e/BITRISE.md @@ -129,10 +129,11 @@ Because exposure also reaches fork builds, keep **Project settings > Builds > Manual approval** enabled so a Shopify admin must approve an outside contribution before any step can access the key. -`e2e/scripts/bitrise_ci_helpers` installs a pinned `ejson2env`, verifies its -checksum, writes the key into `EJSON_KEYDIR`, and runs `scripts/generate_env_files`. -That generates `.env` and `e2e/.env`; neither the key nor decrypted values enter -an argument list or build log. +`e2e/scripts/bitrise_ci_helpers` requires the pinned `ejson2env` version. It +warns and installs the pin if another version is present, verifies the archive +against a checksum committed in the helper, writes the key into `EJSON_KEYDIR`, +and runs `scripts/generate_env_files`. That generates `.env` and `e2e/.env`; +neither the key nor decrypted values enter an argument list or build log. Both committed EJSON files must use the same keypair because one Bitrise secret cannot hold two private keys. To change a value, run `dev secrets edit demo` or @@ -190,4 +191,4 @@ The pipeline uses Bitrise cache steps for key-based pnpm/CocoaPods/Gradle cache Do not add `activate-build-cache-for-xcode` or `activate-build-cache-for-gradle`; the Bitrise Build Cache add-on is disabled for Shopify Bitrise apps. -Ruby and Node versions are pinned in `e2e/bitrise.yml` via the Bitrise `tools:` configuration (`ruby: 3.3.6`, `nodejs: 22.14.0`), which Bitrise installs before each workflow runs. Pin exact versions that the target stacks preinstall so setup stays fast and reproducible; a version the stack does not ship is installed on demand and is slower. pnpm is pinned separately through Corepack via the `packageManager` field in `platforms/react-native/package.json`. +Ruby and Node versions are configured in `e2e/bitrise.yml` via Bitrise `tools:` (`ruby: 3.4:installed`, `nodejs: 22.14.0`). pnpm is pinned separately through Corepack via the `packageManager` field in `platforms/react-native/package.json`. diff --git a/e2e/README.md b/e2e/README.md index 527752818..3c7cedc6c 100644 --- a/e2e/README.md +++ b/e2e/README.md @@ -15,21 +15,14 @@ same environment contract through the BrowserStack run plan. ## Encrypted environments -`dev up` generates the sample-app `.env` and the suite-specific `e2e/.env` from -committed files under `config/secrets`. Both are generated, so direct edits are -lost on the next `dev up`. +`dev up` generates `.env` and `e2e/.env` from `config/secrets`. -To change a shared value, run `dev secrets edit demo` or `dev secrets edit e2e`, -then run `./scripts/ejson_lint` before committing the encrypted file. The lint -command needs no private key and fails if plaintext was committed. +Use `dev secrets edit demo` or `dev secrets edit e2e` for shared changes, and +`.env.local` for gitignored sample-app overrides. Run `./scripts/ejson_lint` +before committing encrypted configuration changes. -Use `.env.local` for personal sample-app overrides and `e2e/.env.local` for -personal E2E overrides. Nothing writes either file. - -CI configures the sample apps from `e2e/.env` with -`scripts/setup_storefront_env --env-file e2e/.env --ignore-generated`. This keeps -E2E runs on their own store and prevents previously generated demo configuration -from becoming an input. +CI builds sample apps from `e2e/.env`; local Maestro runs use the configuration +already generated for the sample app. ## Run locally @@ -161,7 +154,6 @@ ruby e2e/scripts/e2e_matrix_to_browserstack_run_plan count - `config.yaml` configures Maestro for shared platform behavior and quarantines the `flaky` and `wip` tags. - `flows/` contains reusable Maestro subflows for app setup and checkout steps. -<<<<<<< HEAD - `tests/shared/` holds the tests every target runs through the CI matrix. - `tests//` holds platform-local tests. The matrix may ignore their tags. - `tests/shared/launch-smoke.yaml` is the shared launch smoke test. @@ -174,19 +166,8 @@ ruby e2e/scripts/e2e_matrix_to_browserstack_run_plan count - `scripts/run_local_e2e` builds and installs any of the four local targets. - `scripts/run_maestro` is their single Maestro invocation. It holds the environment contract and target-specific test-file selection in one place. -||||||| parent of 0e27e5e2 (Build E2E apps from e2e/.env, not the demo store) -- `tests/react-native/checkout-guest.yaml` composes the React Native guest - checkout smoke test from those subflows. -- `tests/react-native/checkout-hardcoded-buyer-identity.yaml` verifies checkout - from a bootstrapped cart with hardcoded buyer identity. -======= -- `tests/react-native/checkout-guest.yaml` composes the React Native guest - checkout smoke test from those subflows. -- `tests/react-native/checkout-hardcoded-buyer-identity.yaml` verifies checkout - from a bootstrapped cart with hardcoded buyer identity. -- `scripts/bitrise_ci_helpers` holds the shared shell functions the CI build steps use, - including `e2e_configure_storefront`. ->>>>>>> 0e27e5e2 (Build E2E apps from e2e/.env, not the demo store) +- `scripts/bitrise_ci_helpers` holds shared functions used by CI builds, including + EJSON setup and `e2e_configure_storefront`. - `config/matrix.yml`, `lib/e2e_matrix_to_browserstack_run_plan.rb`, and `scripts/` drive the BrowserStack run plan. diff --git a/e2e/scripts/bitrise_ci_helpers b/e2e/scripts/bitrise_ci_helpers index 6105bc603..b807f9bbe 100755 --- a/e2e/scripts/bitrise_ci_helpers +++ b/e2e/scripts/bitrise_ci_helpers @@ -22,52 +22,55 @@ e2e_deploy_dir() { } e2e_sha256() { - if command -v sha256sum >/dev/null 2>&1; then - sha256sum "$1" | awk '{print $1}' - else - shasum -a 256 "$1" | awk '{print $1}' - fi + ruby -rdigest -e 'puts Digest::SHA256.file(ARGV.fetch(0)).hexdigest' "$1" } # Neither Bitrise stack preinstalls ejson2env, and both are ephemeral, so every # workflow that decrypts installs it. Only ejson2env is needed: nothing in CI # encrypts, and scripts/ejson_lint reads the ciphertext without a binary. e2e_install_ejson2env() { - local os arch asset base archive checksums expected actual dir + local installed_version host release_platform asset base archive expected actual dir if command -v ejson2env >/dev/null 2>&1; then - e2e_log "ejson2env is already installed" - return 0 - fi + installed_version="$(ejson2env --version 2>/dev/null | awk '{print $NF; exit}')" + if [ "$installed_version" = "$E2E_EJSON2ENV_VERSION" ]; then + e2e_log "ejson2env ${E2E_EJSON2ENV_VERSION} is already installed" + return 0 + fi - os="$(uname -s | tr '[:upper:]' '[:lower:]')" - arch="$(uname -m)" + echo "e2e_install_ejson2env: warning: found ejson2env ${installed_version:-with an unknown version}; expected ${E2E_EJSON2ENV_VERSION}. Installing the pinned version." >&2 + fi - case "$arch" in - x86_64) arch="amd64" ;; - aarch64 | arm64) arch="arm64" ;; + host="$(uname -s)/$(uname -m)" + case "$host" in + Darwin/*) + release_platform="darwin_all" + expected="98734569cde6075bb5cf43a2aa83ebec2ad91959a43eb077538fd8feda850af7" + ;; + Linux/x86_64) + release_platform="linux_amd64" + expected="d06d1090733658f2f493d20d6262c276bd776aff84f580e1d9e4acfd1d399398" + ;; + Linux/aarch64 | Linux/arm64) + release_platform="linux_arm64" + expected="95a5fd97c7390341e9c6ad2489c84e079ff9e97ddfa7e1a8e35b6a7b571afe1d" + ;; + *) + echo "e2e_install_ejson2env: no pinned archive for ${host}" >&2 + return 1 + ;; esac - # One universal build covers both Apple architectures. - if [ "$os" = "darwin" ]; then - arch="all" - fi + asset="ejson2env_${E2E_EJSON2ENV_VERSION}_${release_platform}.tar.gz" - asset="ejson2env_${E2E_EJSON2ENV_VERSION}_${os}_${arch}.tar.gz" base="https://github.com/Shopify/ejson2env/releases/download/v${E2E_EJSON2ENV_VERSION}" archive="$(mktemp -d)/${asset}" - checksums="$(dirname "$archive")/checksums.txt" - e2e_log "Installing ejson2env ${E2E_EJSON2ENV_VERSION} for ${os}/${arch}" + e2e_log "Installing ejson2env ${E2E_EJSON2ENV_VERSION} for ${host}" curl -fsSL --retry 3 -o "$archive" "${base}/${asset}" - curl -fsSL --retry 3 -o "$checksums" "${base}/ejson2env_${E2E_EJSON2ENV_VERSION}_checksums.txt" - # This binary decrypts every secret in the run, so a tampered release must stop - # the build rather than reach the keydir. - expected="$(awk -v name="$asset" '$2 == name {print $1}' "$checksums")" actual="$(e2e_sha256 "$archive")" - - if [ -z "$expected" ] || [ "$expected" != "$actual" ]; then + if [ "$expected" != "$actual" ]; then echo "e2e_install_ejson2env: checksum mismatch for ${asset}" >&2 return 1 fi @@ -77,6 +80,12 @@ e2e_install_ejson2env() { tar -xzf "$archive" -C "$dir" ejson2env chmod +x "$dir/ejson2env" + installed_version="$("$dir/ejson2env" --version 2>/dev/null | awk '{print $NF; exit}')" + if [ "$installed_version" != "$E2E_EJSON2ENV_VERSION" ]; then + echo "e2e_install_ejson2env: installed version ${installed_version:-unknown}; expected ${E2E_EJSON2ENV_VERSION}" >&2 + return 1 + fi + export PATH="$dir:$PATH" envman add --key PATH --value "$PATH" } diff --git a/e2e/scripts/test_bitrise_ci_helpers b/e2e/scripts/test_bitrise_ci_helpers deleted file mode 100755 index e5bd50798..000000000 --- a/e2e/scripts/test_bitrise_ci_helpers +++ /dev/null @@ -1,137 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -# Synthetic tests for e2e/scripts/bitrise_ci_helpers. -# -# The guarantee here is the separation between the two stores. The sample apps -# are configured from the repo-root .env, which comes from config/secrets/demo.ejson -# and which a developer may point at their own shop. The E2E suite has its own -# shop in config/secrets/e2e.ejson. A suite run that read the repo-root .env would -# test whichever store happened to be configured for manual work. -# -# scripts/test_setup_storefront_env proves --env-file reads the file it is given. -# This file proves the E2E path passes it. -# -# The helpers only define functions when sourced, so no CI environment is needed. - -REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" -HELPERS="${REPO_ROOT}/e2e/scripts/bitrise_ci_helpers" - -fixtures=() - -cleanup() { - local fixture - if [[ "${#fixtures[@]}" -eq 0 ]]; then - return - fi - - for fixture in "${fixtures[@]}"; do - rm -rf "$fixture" - done -} - -trap cleanup EXIT - -fail() { - echo "test_bitrise_ci_helpers: $1" >&2 - exit 1 -} - -# A fake scripts/setup_storefront_env that records its argv instead of writing -# any platform configuration. -make_fixture() { - local fixture - fixture="$(mktemp -d "${TMPDIR:-/tmp}/checkout-kit-ci-helpers.XXXXXX")" - fixtures+=("$fixture") - - mkdir -p "$fixture/scripts" "$fixture/e2e" - - cat >"$fixture/scripts/setup_storefront_env" <<'EOF' -#!/usr/bin/env bash -printf '%s\n' "$@" >"${ARGV_LOG}" -EOF - chmod +x "$fixture/scripts/setup_storefront_env" - - printf '%s\n' "$fixture" -} - -# Runs e2e_configure_storefront with the decrypt step stubbed out, so the test -# needs no key and no network. -run_configure_storefront() { - local fixture="$1" - - ( - source "$HELPERS" - - e2e_generate_env_files() { :; } - e2e_log() { :; } - - cd "$fixture" - ARGV_LOG="$fixture/argv.log" e2e_configure_storefront - ) -} - -argv_of() { - cat "$1/argv.log" -} - -test_configure_storefront_reads_the_e2e_env_file() { - local fixture argv - fixture="$(make_fixture)" - - run_configure_storefront "$fixture" - - [[ -f "$fixture/argv.log" ]] || fail "setup_storefront_env was never called" - - argv="$(argv_of "$fixture")" - - grep -Fxq -- "--env-file" <<<"$argv" || - fail "e2e_configure_storefront did not pass --env-file, so the suite reads the repo-root .env" - - grep -Fxq -- "e2e/.env" <<<"$argv" || - fail "e2e_configure_storefront passed --env-file but not e2e/.env" -} - -# The flag and its value must be adjacent, or the value lands on some other flag. -test_env_file_value_follows_the_flag() { - local fixture - fixture="$(make_fixture)" - - run_configure_storefront "$fixture" - - grep -A 1 -Fx -- "--env-file" "$fixture/argv.log" | grep -Fxq -- "e2e/.env" || - fail "the value after --env-file is not e2e/.env" -} - -# Generated platform config is a value source. The suite must not inherit values -# from a previous demo-store run. -test_configure_storefront_ignores_generated_config() { - local fixture - fixture="$(make_fixture)" - - run_configure_storefront "$fixture" - - grep -Fxq -- "--ignore-generated" "$fixture/argv.log" || - fail "e2e_configure_storefront did not pass --ignore-generated, so stale demo values can win" -} - -test_configure_storefront_never_names_the_repo_root_env() { - local fixture line - fixture="$(make_fixture)" - - run_configure_storefront "$fixture" - - while IFS= read -r line; do - if [[ "$line" == ".env" || "$line" == "./.env" || "$line" == */checkout-kit/.env ]]; then - fail "e2e_configure_storefront pointed setup_storefront_env at the repo-root .env" - fi - done <"$fixture/argv.log" -} - -test_configure_storefront_reads_the_e2e_env_file -test_env_file_value_follows_the_flag -test_configure_storefront_ignores_generated_config -test_configure_storefront_never_names_the_repo_root_env - -echo "bitrise_ci_helpers synthetic tests passed." diff --git a/platforms/swift/Samples/README.md b/platforms/swift/Samples/README.md index a6301a212..bd7560dea 100644 --- a/platforms/swift/Samples/README.md +++ b/platforms/swift/Samples/README.md @@ -65,5 +65,5 @@ in Xcode, then build and run the `ShopifyAcceleratedCheckoutsApp` scheme. | Build log output | Cause | Fix | | --- | --- | --- | | `Storefront.xcconfig: no such file or directory` | `Storefront.xcconfig` file is missing. | Run `dev up` from the repo root or any platform directory. | -| `STOREFRONT_DOMAIN` is blank in generated entitlements | `Storefront.xcconfig` exists but `STOREFRONT_DOMAIN` is blank. | Set it in `.env.local`, then run `dev up`. Root `.env` is generated, so an edit there is lost. | -| Associated domains not working at runtime | Domain value is incorrect. | Set the right value in `.env.local`, then run `dev up`. | +| `STOREFRONT_DOMAIN` is blank in generated entitlements | `Storefront.xcconfig` exists but `STOREFRONT_DOMAIN` is blank. | Set it in `.env.local` (Shopify employees) or `.env` (external contributors), then rerun setup. | +| Associated domains not working at runtime | Domain value is incorrect. | Correct it in `.env.local` (Shopify employees) or `.env` (external contributors), then rerun setup. | diff --git a/scripts/setup_storefront_env b/scripts/setup_storefront_env index 8d538bfb7..1b6f921d3 100755 --- a/scripts/setup_storefront_env +++ b/scripts/setup_storefront_env @@ -17,22 +17,11 @@ usage() { cat <"$fixture/e2e/.env" <<'EOF' STOREFRONT_DOMAIN=e2e-store.example.myshopify.com STOREFRONT_ACCESS_TOKEN=e2e-token -API_VERSION=2026-04 EOF "$fixture/scripts/setup_storefront_env" --env-file "$fixture/e2e/.env" >"$output" 2>&1 - assert_output_is_sanitized "$output" + assert_output_is_sanitized "$output" assert_file_is_unchanged "$fixture/.env" "$root_before" assert_contains "$android_env" "STOREFRONT_DOMAIN=e2e-store.example.myshopify.com" assert_not_contains "$android_env" "synthetic-store.example.myshopify.com" @@ -648,25 +646,24 @@ EOF printf 'STOREFRONT_DOMAIN=%s\n' e2e-local-store.example.myshopify.com >"$fixture/e2e/.env.local" "$fixture/scripts/setup_storefront_env" --env-file "$fixture/e2e/.env" >"$output" 2>&1 - assert_output_is_sanitized "$output" + assert_output_is_sanitized "$output" assert_contains "$android_env" "STOREFRONT_DOMAIN=e2e-local-store.example.myshopify.com" assert_not_contains "$android_env" "overridden-store.example.myshopify.com" } -# Generated platform config is a fallback so a developer who lost .env keeps -# their values. An isolated E2E run must be able to ignore that stale input. -test_env_file_ignores_stale_generated_config() { - local fixture output android_env react_native_env +test_ignore_generated_excludes_generated_sources() { + local fixture output react_native_env react_native_xcconfig fixture="$(make_fixture)" output="$fixture/output.log" - android_env="$(android_env_of "$fixture")" react_native_env="$(react_native_env_of "$fixture")" + react_native_xcconfig="$(react_native_xcconfig_of "$fixture")" cat >"$react_native_env" <<'EOF' STOREFRONT_DOMAIN=stale-demo.example.myshopify.com STOREFRONT_ACCESS_TOKEN=stale-demo-token EOF + printf 'APPLE_PAY_MERCHANT_IDENTIFIER = inherited-merchant\n' >"$react_native_xcconfig" mkdir -p "$fixture/e2e" cat >"$fixture/e2e/.env" <<'EOF' @@ -676,36 +673,11 @@ EOF "$fixture/scripts/setup_storefront_env" \ --env-file "$fixture/e2e/.env" --ignore-generated >"$output" 2>&1 - assert_output_is_sanitized "$output" - - assert_contains "$android_env" "STOREFRONT_DOMAIN=e2e-store.example.myshopify.com" - assert_contains "$android_env" "STOREFRONT_ACCESS_TOKEN=e2e-token" - assert_not_contains "$android_env" "stale-demo.example.myshopify.com" - assert_not_contains "$android_env" "stale-demo-token" -} - -# The demo path keeps inheriting, so a developer who lost .env does not silently -# lose their configured store. -test_generated_config_is_still_inherited_without_the_flag() { - local fixture output react_native_env - fixture="$(make_fixture)" - output="$fixture/output.log" - react_native_env="$(react_native_env_of "$fixture")" - - cat >"$react_native_env" <<'EOF' -APPLE_PAY_MERCHANT_IDENTIFIER=inherited-merchant -EOF - mkdir -p "$fixture/e2e" - cat >"$fixture/e2e/.env" <<'EOF' -STOREFRONT_DOMAIN=e2e-store.example.myshopify.com -STOREFRONT_ACCESS_TOKEN=e2e-token -EOF - - "$fixture/scripts/setup_storefront_env" --env-file "$fixture/e2e/.env" >"$output" 2>&1 assert_output_is_sanitized "$output" - - assert_contains "$react_native_env" "APPLE_PAY_MERCHANT_IDENTIFIER=\"inherited-merchant\"" + assert_contains "$react_native_env" 'STOREFRONT_DOMAIN="e2e-store.example.myshopify.com"' + assert_not_contains "$react_native_env" "stale-demo.example.myshopify.com" + assert_not_contains "$react_native_xcconfig" "inherited-merchant" } test_env_file_requires_a_path() { @@ -716,6 +688,7 @@ test_env_file_requires_a_path() { if "$fixture/scripts/setup_storefront_env" --env-file >"$output" 2>&1; then fail "--env-file was accepted with no path" fi + assert_contains "$output" "Usage:" } @@ -738,8 +711,7 @@ test_env_local_is_never_baked_into_root_env test_legacy_merchant_identifier_configures_both_samples_without_rewriting_root_env test_env_file_selects_an_alternate_source test_env_file_takes_its_own_local_overlay -test_env_file_ignores_stale_generated_config -test_generated_config_is_still_inherited_without_the_flag +test_ignore_generated_excludes_generated_sources test_env_file_requires_a_path echo "setup_storefront_env synthetic tests passed."