Skip to content
Open
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
12 changes: 7 additions & 5 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
# 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.
#
# **External contributors.** Run `cp .env.example .env`, add your storefront
# details, then run `scripts/setup_storefront_env`.
#
# **Shopify employees.** Run `dev up`; use `.env.local` for gitignored overrides.
#
# Every key is optional except the two storefront details.
#
# Do not commit real values from .env or generated platform config files.

Expand Down
27 changes: 18 additions & 9 deletions .github/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +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 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
`.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 <command>`, `dev swift <command>`, `dev react-native <command>` (or `dev rn`), and `dev web <command>` after setup. Protocol schema/model commands are available as `dev protocol <command>`. For cross-platform changes, use `dev lint`, `dev test`, `dev check`, `dev format`, and `dev build`.

Expand Down Expand Up @@ -133,10 +143,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.

---

Expand Down
30 changes: 23 additions & 7 deletions e2e/BITRISE.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,14 +114,30 @@ 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` 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
`dev secrets edit e2e` and commit the encrypted file.

## BrowserStack secrets

Expand Down Expand Up @@ -175,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`.
13 changes: 13 additions & 0 deletions e2e/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,17 @@ 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 `.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 sample-app overrides. Run `./scripts/ejson_lint`
before committing encrypted configuration changes.

CI builds sample apps from `e2e/.env`; local Maestro runs use the configuration
already generated for the sample app.

## Run locally

Run `dev up` first to provision the local toolchain, including the pinned Maestro
Expand Down Expand Up @@ -155,6 +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.
- `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.

Expand Down
147 changes: 142 additions & 5 deletions e2e/scripts/bitrise_ci_helpers
Original file line number Diff line number Diff line change
Expand Up @@ -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' "$*"
}
Expand All @@ -17,12 +21,145 @@ e2e_deploy_dir() {
printf '%s\n' "$dir"
}

e2e_sha256() {
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 installed_version host release_platform asset base archive expected actual dir

if command -v ejson2env >/dev/null 2>&1; then
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

echo "e2e_install_ejson2env: warning: found ejson2env ${installed_version:-with an unknown version}; expected ${E2E_EJSON2ENV_VERSION}. Installing the pinned version." >&2
fi

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

asset="ejson2env_${E2E_EJSON2ENV_VERSION}_${release_platform}.tar.gz"

base="https://github.com/Shopify/ejson2env/releases/download/v${E2E_EJSON2ENV_VERSION}"
archive="$(mktemp -d)/${asset}"

e2e_log "Installing ejson2env ${E2E_EJSON2ENV_VERSION} for ${host}"
curl -fsSL --retry 3 -o "$archive" "${base}/${asset}"

actual="$(e2e_sha256 "$archive")"
if [ "$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"

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"
}

# 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.}"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is already set but is a guard incase we start up another pipeline


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
}

# 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_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_log "Configuring storefront environment"
./scripts/setup_storefront_env
e2e_generate_env_files

e2e_log "Configuring storefront environment from e2e/.env"
./scripts/setup_storefront_env --env-file e2e/.env --ignore-generated
}

e2e_nightly_commit_window() {
Expand Down
4 changes: 2 additions & 2 deletions platforms/swift/Samples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` (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. |
Loading
Loading