diff --git a/dev.yml b/dev.yml index f8408ae8..2a3d1242 100644 --- a/dev.yml +++ b/dev.yml @@ -9,6 +9,7 @@ up: - swiftlint - swiftformat - sccache + - bitrise - ruby - xcode: version: "26.2" @@ -47,6 +48,7 @@ check: react-native-lint-sample: cd platforms/react-native && pnpm sample lint web-lint: cd platforms/web && pnpm lint web-test: cd platforms/web && pnpm test + e2e-bitrise-validate: bitrise validate --config=e2e/bitrise.yml commands: # Repo-wide diff --git a/e2e/BITRISE.md b/e2e/BITRISE.md new file mode 100644 index 00000000..237ea9c1 --- /dev/null +++ b/e2e/BITRISE.md @@ -0,0 +1,64 @@ +# Bitrise E2E Pipeline + +Checkout Kit E2E tests run in Bitrise through the `e2e` pipeline, with this app-level setup. + +## Bitrise app + +The pipeline runs on the allocated Bitrise app, connected to the Checkout Kit repository: + +- Bitrise app: https://app.bitrise.io/app/f51f9054-053e-40f1-81e9-ae727567ae76 +- Repository: `Shopify/checkout-kit` + +Useful Bitrise app URLs: + +| Area | URL | +|---|---| +| App overview | https://app.bitrise.io/app/f51f9054-053e-40f1-81e9-ae727567ae76 | +| Workflow/config editor | https://app.bitrise.io/app/f51f9054-053e-40f1-81e9-ae727567ae76/workflow | +| Secrets/env vars | https://app.bitrise.io/app/f51f9054-053e-40f1-81e9-ae727567ae76/secrets | +| Code signing | https://app.bitrise.io/app/f51f9054-053e-40f1-81e9-ae727567ae76/codesigning | +| Build triggers | https://app.bitrise.io/app/f51f9054-053e-40f1-81e9-ae727567ae76/triggers | +| Start build | https://app.bitrise.io/app/f51f9054-053e-40f1-81e9-ae727567ae76/build/start | + +If a direct URL does not resolve in the current Bitrise UI, open the app overview and navigate to the matching area from the sidebar. + +## Pipeline + +The `e2e` pipeline is defined in `e2e/bitrise.yml`, and the Bitrise app reads its configuration directly from that repository path: + +```text +e2e/bitrise.yml +``` + +The pipeline defines the E2E workflow graph and the intermediate artifacts each workflow passes to the next. + +Default stacks and machine types live in `e2e/bitrise.yml`; workflows run on Linux unless they require macOS-specific tooling. + +Validate the configuration locally with: + +```bash +bitrise validate -c e2e/bitrise.yml +``` + +## Required app environment variables + +The non-secret E2E defaults live in `e2e/bitrise.yml` under `app.envs`. Change them in this repository rather than in the Bitrise Workflow Editor. + +| Variable | Value | Purpose | +|---|---|---| +| `E2E_STRICT` | `false` | Soft/hard failure switch for BrowserStack Maestro runs. | +| `E2E_BROWSERSTACK_API_RETRIES` | `1` | Retries for transient BrowserStack API responses. | +| `E2E_BROWSERSTACK_TIMEOUT_SECONDS` | `1800` | BrowserStack build timeout. | +| `E2E_BROWSERSTACK_POLL_SECONDS` | `30` | BrowserStack status polling interval. | + +Each workflow's main `script` step sets its own wall-clock budget with the Bitrise `timeout` and `no_output_timeout` step properties instead of wrapping individual commands. + +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. + +## Caching + +The pipeline uses Bitrise cache steps for key-based pnpm/CocoaPods/Gradle cache paths. + +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`. diff --git a/e2e/README.md b/e2e/README.md index eb942c00..3b016d73 100644 --- a/e2e/README.md +++ b/e2e/README.md @@ -61,9 +61,10 @@ the React Native sample configuration, not local in-repo native SDK overrides. ## Matrix CI runs are described by `config/matrix.yml`. The matrix expands applications, OS -version tags, and suites into BrowserStack Maestro run rows. Because Bitrise has no -built-in matrix support, `e2e/lib/e2e_matrix.rb` performs the expansion and the -pipeline parallelizes over the resulting rows. +version tags, and suites into a BrowserStack run plan. Because Bitrise has no +built-in matrix support, `e2e/lib/e2e_matrix_to_browserstack_run_plan.rb` +transforms the matrix into a BrowserStack run plan and the pipeline +parallelizes over the resulting rows. Current applications: @@ -81,19 +82,25 @@ Current suites: Validate the matrix: ```bash -ruby e2e/scripts/e2e_matrix validate +ruby e2e/scripts/e2e_matrix_to_browserstack_run_plan validate ``` -Expand all run rows: +Expand the BrowserStack run plan: ```bash -ruby e2e/scripts/e2e_matrix expand +ruby e2e/scripts/e2e_matrix_to_browserstack_run_plan expand ``` -Expand a single run row by index: +Expand a single BrowserStack run plan row by index: ```bash -ruby e2e/scripts/e2e_matrix expand --index 0 +ruby e2e/scripts/e2e_matrix_to_browserstack_run_plan expand --index 0 +``` + +Count BrowserStack run plan rows: + +```bash +ruby e2e/scripts/e2e_matrix_to_browserstack_run_plan count ``` ## Files @@ -102,7 +109,7 @@ ruby e2e/scripts/e2e_matrix expand --index 0 - `flows/` contains reusable Maestro subflows for app setup and checkout steps. - `tests/react-native/full-guest-checkout.yaml` composes the React Native guest checkout smoke test from those subflows. -- `config/matrix.yml`, `lib/e2e_matrix.rb`, and `scripts/` drive the CI matrix. +- `config/matrix.yml`, `lib/e2e_matrix_to_browserstack_run_plan.rb`, and `scripts/` drive the BrowserStack run plan. - `tests/shared/launch-smoke.yaml` is the shared launch smoke suite. ## Shared app contract diff --git a/e2e/bitrise.yml b/e2e/bitrise.yml new file mode 100644 index 00000000..5cc8b152 --- /dev/null +++ b/e2e/bitrise.yml @@ -0,0 +1,168 @@ +format_version: "23" +default_step_lib_source: https://github.com/bitrise-io/bitrise-steplib.git +project_type: other + +meta: + bitrise.io: + stack: ubuntu-noble-24.04-bitrise-2025-android + machine_type_id: g2.linux.medium + +app: + envs: + - E2E_STRICT: "false" + - E2E_BROWSERSTACK_API_RETRIES: "1" + - E2E_BROWSERSTACK_TIMEOUT_SECONDS: "1800" + - E2E_BROWSERSTACK_POLL_SECONDS: "30" + +tools: + ruby: 3.3.6 + nodejs: 22.14.0 + +pipelines: + e2e: + workflows: + e2e-produce-browserstack-run-plan: {} + e2e-build-react-native-ios: + depends_on: + - e2e-produce-browserstack-run-plan + e2e-build-react-native-android: + depends_on: + - e2e-produce-browserstack-run-plan + e2e-execute-browserstack-run: + depends_on: + - e2e-produce-browserstack-run-plan + - e2e-build-react-native-ios + - e2e-build-react-native-android + parallel: $E2E_BROWSERSTACK_RUN_PLAN_COUNT + e2e-report: + depends_on: + - e2e-execute-browserstack-run + should_always_run: workflow + +workflows: + e2e-produce-browserstack-run-plan: + steps: + - git-clone@8: {} + - script@1: + title: Produce BrowserStack run plan placeholder + timeout: 1800 + no_output_timeout: 1800 + inputs: + - content: |- + set -euo pipefail + source e2e/scripts/bitrise_ci_helpers + ruby e2e/scripts/e2e_matrix_to_browserstack_run_plan validate + e2e_log "Producing BrowserStack run plan" + ruby e2e/scripts/e2e_matrix_to_browserstack_run_plan expand > "$BITRISE_DEPLOY_DIR/browserstack-run-plan.json" + e2e_log "Publishing BrowserStack run plan count" + envman add --key E2E_BROWSERSTACK_RUN_PLAN_COUNT --value "$(ruby e2e/scripts/e2e_matrix_to_browserstack_run_plan count)" + e2e_log "Creating E2E tests zip placeholder" + mkdir -p "$BITRISE_DEPLOY_DIR/e2e" + echo "Phase 2 placeholder for E2E tests zip" > "$BITRISE_DEPLOY_DIR/e2e/e2e-tests.zip" + e2e_log "Publishing E2E tests zip path" + envman add --key E2E_TESTS_ZIP --value "$BITRISE_DEPLOY_DIR/e2e/e2e-tests.zip" + - share-pipeline-variable@1: + inputs: + - variables: E2E_BROWSERSTACK_RUN_PLAN_COUNT + - deploy-to-bitrise-io@2: + is_always_run: false + inputs: + - pipeline_intermediate_files: |- + $E2E_TESTS_ZIP:E2E_TESTS_ZIP + $BITRISE_DEPLOY_DIR/browserstack-run-plan.json:E2E_BROWSERSTACK_RUN_PLAN_JSON + + e2e-build-react-native-ios: + steps: + - git-clone@8: {} + - restore-cache@3: + inputs: + - key: |- + rn-ios-{{ .OS }}-{{ .Arch }}-{{ checksum "platforms/react-native/pnpm-lock.yaml" }}-{{ checksum "platforms/react-native/sample/ios/Podfile.lock" }} + - script@1: + title: Create React Native iOS artifact placeholder + inputs: + - content: |- + set -euo pipefail + mkdir -p "$BITRISE_DEPLOY_DIR/e2e" + echo "Phase 2 placeholder for React Native iOS IPA" > "$BITRISE_DEPLOY_DIR/e2e/react-native-ios.ipa" + envman add --key E2E_REACT_NATIVE_IOS_APP_PATH --value "$BITRISE_DEPLOY_DIR/e2e/react-native-ios.ipa" + - deploy-to-bitrise-io@2: + inputs: + - pipeline_intermediate_files: |- + $E2E_REACT_NATIVE_IOS_APP_PATH:E2E_REACT_NATIVE_IOS_APP_PATH + - save-cache@1: + inputs: + - key: |- + rn-ios-{{ .OS }}-{{ .Arch }}-{{ checksum "platforms/react-native/pnpm-lock.yaml" }}-{{ checksum "platforms/react-native/sample/ios/Podfile.lock" }} + - paths: |- + platforms/react-native/node_modules + platforms/react-native/sample/ios/Pods + + e2e-build-react-native-android: + steps: + - git-clone@8: {} + - restore-cache@3: + inputs: + - key: |- + rn-android-{{ checksum "platforms/react-native/pnpm-lock.yaml" }}-{{ checksum "platforms/react-native/sample/android/**/*.gradle*" }} + - script@1: + title: Create React Native Android artifact placeholder + inputs: + - content: |- + set -euo pipefail + mkdir -p "$BITRISE_DEPLOY_DIR/e2e" + echo "Phase 2 placeholder for React Native Android APK" > "$BITRISE_DEPLOY_DIR/e2e/react-native-android.apk" + envman add --key E2E_REACT_NATIVE_ANDROID_APP_PATH --value "$BITRISE_DEPLOY_DIR/e2e/react-native-android.apk" + - deploy-to-bitrise-io@2: + inputs: + - pipeline_intermediate_files: |- + $E2E_REACT_NATIVE_ANDROID_APP_PATH:E2E_REACT_NATIVE_ANDROID_APP_PATH + - save-cache@1: + inputs: + - key: |- + rn-android-{{ checksum "platforms/react-native/pnpm-lock.yaml" }}-{{ checksum "platforms/react-native/sample/android/**/*.gradle*" }} + - paths: |- + platforms/react-native/node_modules + ~/.gradle/caches + ~/.gradle/wrapper + + e2e-execute-browserstack-run: + steps: + - git-clone@8: {} + - pull-intermediate-files@1: + inputs: + - artifact_sources: |- + e2e-produce-browserstack-run-plan + e2e-build-react-native-* + - script@1: + title: Resolve BrowserStack run plan row placeholder + timeout: 2700 + no_output_timeout: 2700 + inputs: + - content: |- + set -euo pipefail + source e2e/scripts/bitrise_ci_helpers + : "${BITRISE_IO_PARALLEL_INDEX:?BITRISE_IO_PARALLEL_INDEX is required for the parallel e2e-execute-browserstack-run workflow.}" + run_index="$BITRISE_IO_PARALLEL_INDEX" + mkdir -p "$BITRISE_DEPLOY_DIR/e2e/results" + : "${E2E_BROWSERSTACK_RUN_PLAN_JSON:?E2E_BROWSERSTACK_RUN_PLAN_JSON is required. Check e2e-execute-browserstack-run pull-intermediate-files artifact_sources.}" + e2e_log "Resolving BrowserStack run plan row ${run_index}" + ruby -rjson -e 'runs = JSON.parse(File.read(ARGV.fetch(0))); puts JSON.pretty_generate(runs.fetch(Integer(ARGV.fetch(1))))' "$E2E_BROWSERSTACK_RUN_PLAN_JSON" "$run_index" > "$BITRISE_DEPLOY_DIR/e2e/results/run-${run_index}.json" + - deploy-to-bitrise-io@2: + inputs: + - pipeline_intermediate_files: |- + $BITRISE_DEPLOY_DIR/e2e/results:E2E_BROWSERSTACK_RESULTS_DIR + + e2e-report: + steps: + - git-clone@8: {} + - pull-intermediate-files@1: + inputs: + - artifact_sources: |- + e2e-execute-browserstack-run + - script@1: + title: Placeholder E2E report + inputs: + - content: |- + set -euo pipefail + echo "Phase 2 placeholder for aggregate E2E reporting" diff --git a/e2e/lib/e2e_matrix.rb b/e2e/lib/e2e_matrix_to_browserstack_run_plan.rb similarity index 95% rename from e2e/lib/e2e_matrix.rb rename to e2e/lib/e2e_matrix_to_browserstack_run_plan.rb index 3e780dab..6dd8b5f0 100644 --- a/e2e/lib/e2e_matrix.rb +++ b/e2e/lib/e2e_matrix_to_browserstack_run_plan.rb @@ -2,12 +2,12 @@ require "yaml" -# Expands the compact E2E test matrix in config/matrix.yml into one fully-resolved -# run per (application x os_version_tag x suite) combination. +# Transforms the compact E2E matrix in config/matrix.yml into the BrowserStack +# run plan consumed by the parallel Bitrise execution workflow. # # Unlike GitHub Actions, Bitrise has no built-in matrix/strategy support, so we -# do the fan-out ourselves: `expand` produces the JSON list of runs that the -# pipeline consumes, and Bitrise parallelizes over it via `run_at`/`count`. +# do the fan-out ourselves: `expand` produces the JSON list of runs, and Bitrise +# parallelizes over it via `run_at`/`count`. # # Today the numbers make the config and the expansion look equivalent - # 2 applications + 1 os_version_tag + 1 suite, and 2 * 1 * 1 = 2 runs - so it can look @@ -15,7 +15,7 @@ # adding a single os_version_tag (e.g. a minimum-supported OS) transparently duplicates # every application x suite across that OS, turning an additive config change into # a multiplicative set of runs without hand-writing each one. -class E2EMatrix +class E2EMatrixToBrowserStackRunPlan attr_reader :config_path def self.load(config_path) @@ -46,6 +46,10 @@ def run_at(index) runs.fetch(index) end + def count + expand.length + end + def validation_errors errors = [] errors << "version must be 1" unless @config.fetch("version", nil) == 1 diff --git a/e2e/scripts/bitrise_ci_helpers b/e2e/scripts/bitrise_ci_helpers new file mode 100755 index 00000000..1586baa8 --- /dev/null +++ b/e2e/scripts/bitrise_ci_helpers @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +set -euo pipefail + +e2e_log() { + printf '\n==> %s\n' "$*" +} diff --git a/e2e/scripts/e2e_matrix b/e2e/scripts/e2e_matrix_to_browserstack_run_plan similarity index 55% rename from e2e/scripts/e2e_matrix rename to e2e/scripts/e2e_matrix_to_browserstack_run_plan index de97ae08..daf67271 100755 --- a/e2e/scripts/e2e_matrix +++ b/e2e/scripts/e2e_matrix_to_browserstack_run_plan @@ -3,14 +3,14 @@ require "json" require "optparse" -require_relative "../lib/e2e_matrix" +require_relative "../lib/e2e_matrix_to_browserstack_run_plan" options = { config: File.expand_path("../config/matrix.yml", __dir__) } parser = OptionParser.new do |opts| - opts.banner = "Usage: e2e/scripts/e2e_matrix validate|expand [options]" + opts.banner = "Usage: e2e/scripts/e2e_matrix_to_browserstack_run_plan validate|expand|count [options]" opts.on("--config PATH") { |path| options[:config] = path } opts.on("--index INDEX", Integer) { |index| options[:index] = index } end @@ -18,16 +18,16 @@ end command = ARGV.shift parser.parse!(ARGV) -unless ["validate", "expand"].include?(command) +unless ["validate", "expand", "count"].include?(command) warn parser exit 1 end -matrix = E2EMatrix.load(options.fetch(:config)) +run_plan = E2EMatrixToBrowserStackRunPlan.load(options.fetch(:config)) case command when "validate" - errors = matrix.validation_errors + errors = run_plan.validation_errors if errors.empty? puts "E2E matrix is valid" else @@ -35,17 +35,25 @@ when "validate" exit 1 end when "expand" - errors = matrix.validation_errors + errors = run_plan.validation_errors unless errors.empty? warn errors.join("\n") exit 1 end output = if options.key?(:index) - matrix.run_at(options.fetch(:index)) + run_plan.run_at(options.fetch(:index)) else - matrix.expand + run_plan.expand end puts JSON.pretty_generate(output) +when "count" + errors = run_plan.validation_errors + unless errors.empty? + warn errors.join("\n") + exit 1 + end + + puts run_plan.count end