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
11 changes: 11 additions & 0 deletions .ci/changed-file-filters.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,14 @@ e2e:
- *exclude-docs-directory
ciFilters:
- '.ci/changed-file-filters.yml'
# The macOS CI gate on Bitrise. Narrower than the e2e filter on purpose: these files
# decide which macOS jobs run, so a change to one has to rerun them all, while a Maestro
# flow edit under e2e/ must not.
iosCiConfig:
- 'e2e/bitrise.yml'
- 'e2e/config/ios_ci.yml'
- 'e2e/lib/ios_ci_run_plan.rb'
- 'e2e/lib/ios_ci_reporter.rb'
- 'e2e/scripts/ios_ci_run_plan'
- 'e2e/scripts/report_ios_ci_results'
- 'e2e/scripts/bitrise_ci_helpers'
3 changes: 2 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,8 @@ jobs:
- 'e2e/flows/**'
- 'e2e/tests/**'
- 'e2e/config.yaml'
- 'e2e/config/matrix.yml'
- 'e2e/config/**'
- 'e2e/bitrise.yml'
- '.ci/changed-file-filters.yml'
- '.github/workflows/ci.yml'

Expand Down
27 changes: 26 additions & 1 deletion e2e/BITRISE.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,31 @@ For example, editing `platforms/react-native/README.md` matches the coarse `chan

The GitHub checks are kept non-blocking while the suite stabilizes; they become merge-blocking only once the "Checkout Kit E2E" check is marked required in branch protection.

## The `ci-ios` pipeline

`ci-ios` is the second pipeline in `e2e/bitrise.yml`. It runs the four macOS jobs that used to run on GitHub Actions: the Swift package tests, the Swift sample build and test, the React Native iOS sample build, and the React Native iOS tests. It is separate from `e2e` rather than a set of extra workflows inside it, because Bitrise reports one status per pipeline: sharing one would tie a merge gate to the BrowserStack device flake that the E2E checks deliberately keep non-blocking.

### Its trigger carries no `changed_files`

Unlike `e2e`, the `ci-ios` `trigger_map` entry has no filter at all. `ci-ios` is a merge-blocking check, and a required check that never posts leaves a pull request permanently unmergeable — so the pipeline has to start on every pull request, including a docs-only one.

Selection happens inside the pipeline instead. The Linux `ci-ios-plan` workflow reads the pull request's changed files, applies the shared filter groups in `.ci/changed-file-filters.yml` through `e2e/config/ios_ci.yml`, and publishes one `CI_IOS_*` variable per job with `share-pipeline-variable`. Each macOS workflow guards on its own variable with `run_if`. A change that needs no macOS job runs the Linux plan and the report, and nothing else.

This is the same two-layer idea as `e2e` — a cheap first pass, then a precise runtime decision — with the first layer set to "always".

### The check is self-posted

`ci-ios-report` runs with `should_always_run: workflow` and posts the `Checkout Kit iOS` Check Run itself, through `e2e/scripts/report_ios_ci_results`. Bitrise's own pipeline status cannot tell the two kinds of not-run apart:

- a job the plan did not select is a **pass** — there was nothing to build
- a job the plan did select but that never finished is a **failure**

The reporter also fails when `ci-ios-plan` itself fails, rather than reporting green off an empty selection. `e2e/test/ios_ci_reporter_test.rb` pins all three cases.

### Changing which files select which job

Edit `e2e/config/ios_ci.yml`, not the workflows. `e2e/test/ios_ci_run_plan_test.rb` asserts set equality between the variables the plan emits and the `run_if` expressions parsed out of `e2e/bitrise.yml`, so a job added on one side and not the other fails the Ruby tests.

## Duplicate PR build cancellation

Duplicate in-progress PR pipelines are cancelled by Bitrise native Rolling builds rather than a repo-owned cancellation script. Under **Project settings > Builds > Build strategy**, **Abort builds triggered by pull requests** and **Abort running builds** are enabled, so a newer PR build cancels the older one.
Expand Down Expand Up @@ -175,4 +200,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 pinned in `e2e/bitrise.yml` via the Bitrise `tools:` configuration (`ruby: "3.4:installed"`, `nodejs: 22.14.0`), which Bitrise installs before each workflow runs. The `:installed` suffix tells each stack to use its own preinstalled 3.4.x rather than compiling one from source. 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`.
32 changes: 32 additions & 0 deletions e2e/RUNBOOK.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,3 +115,35 @@ The report keeps a single sticky PR comment, identified by a hidden marker, and
in place on every build, so green runs never add a second comment. Because that comment
always carries the Tophat install links, it is posted even on a fully green run — a passing
build stays installable from the PR.

## The iOS check failed or never posted

`Checkout Kit iOS` comes from the `ci-ios` pipeline, described in `BITRISE.md`. Three
layers can break, and the symptom tells you which one. Work down the list in order.

**The check never appears.** The pipeline did not start. Its `trigger_map` entry has no
file filter, so the usual cause is the branch head: Bitrise reads `trigger_map` from the
pull request's own commit, and a branch older than the entry never triggers. Rebase on
`main` and push. The entry also sets `draft_pull_request_enabled: false`, so a draft posts
nothing until it is marked ready.

**The check is red but every job says skipped.** `ci-ios-plan` failed, and the reporter
refuses to call an empty selection green. Open that workflow's log. It fetches the changed
file list from GitHub and reads `e2e/config/ios_ci.yml`, so the usual causes are an expired
build token or a malformed config file.

**The check is red and names a job.** That macOS workflow failed or never finished. The
reporter lists a selected job that produced no result as a failure, so a timeout and a
compile error look different in the summary: a timeout shows as missing, a compile error
shows as failed. Both link back to the Bitrise pipeline.

**The check is green and every job says skipped.** Expected on a change that touches no
iOS input — documentation, Android, or web. `ci-ios-plan` and `ci-ios-report` still run,
which costs about a minute on Linux. To confirm the selection is right rather than empty by
accident, run the plan locally against the same file list:

```bash
ruby e2e/scripts/ios_ci_run_plan selected-jobs --changed-file <path>
```

It prints a comma-separated job list, or nothing when no macOS job is needed.
152 changes: 124 additions & 28 deletions e2e/bitrise.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ meta:

app:
envs:
# One device for every macOS job. GitHub Actions grew two values by accident
# (iPhone 17 Pro for Swift, iPhone 16 Pro for React Native); the port keeps a
# single one so a runtime change cannot break half the jobs.
- CI_IOS_SIMULATOR_DEVICE: iPhone 17 Pro
- E2E_BROWSERSTACK_API_RETRIES: "1"
- E2E_BROWSERSTACK_TIMEOUT_SECONDS: "1800"
- E2E_BROWSERSTACK_POLL_SECONDS: "30"
Expand Down Expand Up @@ -40,6 +44,14 @@ trigger_map:
regex: '^(platforms/(swift|android|react-native)|protocol|e2e|\.ci)/.*|^Package\.(swift|resolved)$'
pipeline: e2e

# Deliberately unfiltered. ci-ios is a required check, and a required check that
# never posts leaves a pull request permanently unmergeable. The cheap Linux
# ci-ios-plan workflow decides which macOS jobs run, and ci-ios-report always posts
# — including for a change that needs no macOS job at all.
- pull_request_source_branch: "*"
draft_pull_request_enabled: false
pipeline: ci-ios

pipelines:
e2e:
# Bitrise's default report name is ci/bitrise/<project_slug>/<event_type>,
Expand Down Expand Up @@ -86,6 +98,25 @@ pipelines:
- e2e-execute-browserstack-run
should_always_run: workflow

# The macOS merge gate. Separate from the e2e pipeline on purpose: Bitrise reports one
# status per pipeline, and sharing would tie the merge gate to the BrowserStack device
# flake that e2e/BITRISE.md deliberately keeps non-blocking.
ci-ios:
status_report_name: "ci/bitrise/<target_id>/<event_type>"
workflows:
ci-ios-plan: {}
ci-ios-swift-package-tests:
run_if:
expression: '{{ enveq "CI_IOS_SWIFT_PACKAGE_TESTS" "true" }}'
depends_on:
- ci-ios-plan
# No run_if: this workflow is the required check, so it posts on every build,
# including one where ci-ios-plan selected nothing and every job was skipped.
ci-ios-report:
depends_on:
- ci-ios-swift-package-tests
should_always_run: workflow

# Deliberately absent from trigger_map: a scheduled build in the Bitrise UI is
# the only thing that starts this, alongside a manual start or the API. Do not
# add a `triggers:` block either — Bitrise rejects a config that mixes it with
Expand Down Expand Up @@ -177,6 +208,21 @@ step_bundles:
inputs:
- key: *rn_cocoapods_cache_key
- paths: platforms/react-native/sample/ios/Pods
# xcodebuild spends the first minute of a test run waiting for a cold simulator to
# finish booting. Booting it up front with `bootstatus -b` moves that wait out of the
# measured build step and fails loudly when the runtime is missing from the stack.
create-ios-simulator:
steps:
- script@1:
title: Create and boot the iOS simulator
inputs:
- content: |-
set -euo pipefail
: "${CI_IOS_SIMULATOR_DEVICE:?CI_IOS_SIMULATOR_DEVICE is required}"
xcrun simctl delete all
simulator_uuid="$(xcrun simctl create CIDevice "$CI_IOS_SIMULATOR_DEVICE")"
xcrun simctl bootstatus "$simulator_uuid" -b
envman add --key CURRENT_SIMULATOR_UUID --value "$simulator_uuid"
bootstrap-mint:
inputs:
- mintfile_dir: ""
Expand All @@ -202,6 +248,80 @@ step_bundles:
- paths: ~/.mint

workflows:
# Runs on the cheap default Linux stack. Every macOS machine this pipeline boots is
# decided here, so a change that needs no iOS job costs one short Linux build.
ci-ios-plan:
steps:
- git-clone@8: {}
- script@1:
title: Decide which iOS jobs run
timeout: 900
no_output_timeout: 450
inputs:
- content: |-
set -euo pipefail
source e2e/scripts/bitrise_ci_helpers
e2e_log "Fetching pull request changed files"
changed_files="$(e2e_changed_files_file)"
ruby e2e/scripts/ios_ci_run_plan validate --changed-files-file "$changed_files"
e2e_log "Asserting the pipeline can run every selected job"
branch_config="$(e2e_branch_bitrise_config)"
ruby e2e/scripts/ios_ci_run_plan assert-pipeline-coverage --pipeline-config "$branch_config" --changed-files-file "$changed_files"
e2e_log "Publishing iOS CI job flags"
bitrise_env_file="$(e2e_deploy_dir)/ios-ci-env.txt"
ruby e2e/scripts/ios_ci_run_plan bitrise-env --changed-files-file "$changed_files" > "$bitrise_env_file"
cat "$bitrise_env_file"
while IFS='=' read -r key value; do
envman add --key "$key" --value "$value"
done < "$bitrise_env_file"
- share-pipeline-variable@1:
inputs:
- variables: |-
CI_IOS_HAS_JOBS
CI_IOS_SELECTED_JOBS
CI_IOS_SWIFT_PACKAGE_TESTS

ci-ios-swift-package-tests:
meta:
bitrise.io:
# Keep in sync with dev.yml's xcode.version (26.2). Bitrise stable stacks
# only receive Xcode patch updates, so this pins Xcode 26.2 / iOS 26.2
# (23C54) to match local development.
stack: &macos_stack osx-xcode-26.2.x
machine_type_id: g2.mac.4large
steps:
- git-clone@8: {}
- bundle::create-ios-simulator: {}
- script@1:
title: Run Swift package tests
timeout: 3600
no_output_timeout: 1800
inputs:
- content: |-
set -euo pipefail
cd platforms/swift
./Scripts/xcode_run test ShopifyCheckoutKit-Package

ci-ios-report:
steps:
- git-clone@8: {}
- script@1:
title: Report iOS CI results to GitHub
timeout: 900
no_output_timeout: 450
inputs:
- content: |-
set -euo pipefail
source e2e/scripts/bitrise_ci_helpers
e2e_export_github_token
: "${BITRISE_GIT_COMMIT:?BITRISE_GIT_COMMIT is required}"
# The report names the stages that failed from this roster. Bitrise documents no
# enum for its status field, so log the payload to confirm the literals it uses.
e2e_log "Pipeline stage roster"
printf '%s\n' "${BITRISEIO_FINISHED_WORKFLOWS:-<unset>}"
e2e_log "Reporting iOS CI results to GitHub"
ruby e2e/scripts/report_ios_ci_results

e2e-produce-browserstack-run-plan:
steps:
- git-clone@8: {}
Expand All @@ -213,28 +333,11 @@ workflows:
- content: |-
set -euo pipefail
source e2e/scripts/bitrise_ci_helpers
changed_files="$(e2e_deploy_dir)/changed-files.txt"
e2e_log "Fetching pull request changed files"
# PR builds: the checkout is a shallow merge commit, so the GitHub API
# is the authoritative source for the PR's changed files. Non-PR runs
# (manual, scheduled, local) fall back to diffing the branch against its
# base with a three-dot range (changes since the merge-base, not tip-to-tip).
if [ -n "${BITRISE_PULL_REQUEST:-}" ]; then
ruby e2e/scripts/github_pr_changed_files > "$changed_files"
else
git diff --name-only "${BITRISE_GIT_BRANCH_DEST:-origin/main}...HEAD" > "$changed_files"
fi
changed_files="$(e2e_changed_files_file)"
ruby e2e/scripts/e2e_matrix_to_browserstack_run_plan validate --changed-files-file "$changed_files"
# Guard against config-vs-matrix drift: Bitrise resolves the pipeline graph from
# the branch-head bitrise.yml (before the merge checkout), while the matrix here is
# generated from the merged tree. A stale branch can select a target whose build
# workflow does not exist in the graph, which fails cryptically later in
# e2e-execute-browserstack-run. Fail fast against the branch-head config instead.
e2e_log "Asserting the pipeline can build every selected target"
branch_config="$(e2e_deploy_dir)/branch-bitrise.yml"
if [ -z "${BITRISE_PULL_REQUEST:-}" ] || [ -z "${BITRISE_GIT_COMMIT:-}" ] || ! git show "${BITRISE_GIT_COMMIT}:e2e/bitrise.yml" > "$branch_config" 2>/dev/null; then
cp e2e/bitrise.yml "$branch_config"
fi
branch_config="$(e2e_branch_bitrise_config)"
ruby e2e/scripts/e2e_matrix_to_browserstack_run_plan assert-pipeline-coverage --pipeline-config "$branch_config" --changed-files-file "$changed_files"
e2e_log "Producing BrowserStack run plan"
ruby e2e/scripts/e2e_matrix_to_browserstack_run_plan expand --changed-files-file "$changed_files" > "$BITRISE_DEPLOY_DIR/browserstack-run-plan.json"
Expand Down Expand Up @@ -269,10 +372,7 @@ workflows:
e2e-build-react-native-ios:
meta:
bitrise.io:
# Keep in sync with dev.yml's xcode.version (26.2). Bitrise stable stacks
# only receive Xcode patch updates, so this pins Xcode 26.2 / iOS 26.2
# (23C54) to match local development.
stack: &macos_stack osx-xcode-26.2.x
stack: *macos_stack
machine_type_id: g2.mac.4large
steps:
- git-clone@8: {}
Expand Down Expand Up @@ -434,11 +534,7 @@ workflows:
- content: |-
set -euo pipefail
source e2e/scripts/bitrise_ci_helpers
e2e_log "Checking GitHub reporting configuration"
if [ -z "${GITHUB_TOKEN:-}" ]; then
: "${GIT_HTTP_PASSWORD:?GITHUB_TOKEN or Bitrise GitHub App GIT_HTTP_PASSWORD is required. Enable Project settings > Repository > Extend GitHub App permissions to builds.}"
export GITHUB_TOKEN="$GIT_HTTP_PASSWORD"
fi
e2e_export_github_token
: "${BITRISE_GIT_COMMIT:?BITRISE_GIT_COMMIT is required}"
results_root="${E2E_BROWSERSTACK_RESULTS_DIR:-$BITRISE_DEPLOY_DIR/e2e/results}"
# The report names the stages that failed from this roster. Bitrise documents no
Expand Down
18 changes: 18 additions & 0 deletions e2e/config/ios_ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
version: 1
changed_file_filters: .ci/changed-file-filters.yml
# One entry per macOS CI job the Bitrise ci-ios pipeline runs. Each id maps to a
# ci-ios-<id> workflow in e2e/bitrise.yml and to a CI_IOS_<ID> run_if flag, and
# e2e/test/ios_ci_run_plan_test.rb fails when the two sides drift apart.
#
# These filters mirror the GitHub Actions `changes` job in .github/workflows/ci.yml.
# The iOS jobs deliberately subscribe to iosCiConfig rather than the broad e2e filter,
# so editing a Maestro flow does not boot a macOS machine.
jobs:
- id: swift-package-tests
changed_files_filters:
- swift
- protocolSwift
- protocolShared
- packageSwift
- ciFilters
- iosCiConfig
Loading
Loading