Skip to content

Commit bc06cd5

Browse files
authored
chore(ci): migrate iOS CI runners from macOS Sequoia to Tahoe for Xcode 26.x support (#28433)
--- ## **Description** <!-- Write a short description of the changes included in this pull request, also include relevant motivation and context. Have in mind the following questions: 1. What is the reason for the change? 2. What is the improvement/solution? --> Apple requires all iOS and iPadOS apps to be built with the iOS 26 SDK (Xcode 26 or later) starting April 28, 2026. A previous commit (#27726) bumped the pinned Xcode version from `16.3` to `26.3`, but all iOS CI runner images were still pointing to macOS Sequoia (`sequoia` / `sequoia-xl`). The Sequoia images only ship with Xcode 16.x, so `xcodes select 26.3` (or `xcode-select -s /Applications/Xcode_26.3.app`) would fail with "invalid developer directory". This PR migrates every iOS CI runner image from `ghcr.io/cirruslabs/macos-runner:sequoia[-xl]` to `ghcr.io/cirruslabs/macos-runner:tahoe[-xl]`, which ships with the 3 latest Xcode versions including 26.3 and the `xcodes` CLI tool. The Xcode selection step in `build.yml` is also updated to use `xcodes select 26.3` per the [Cirrus Labs recommended approach](https://cirrus-runners.app/setup/) for their multi-Xcode runner images. The `actionlint.yaml` runner allowlist is updated accordingly to prevent false lint failures. **Files changed:** - `.github/actionlint.yaml` — update self-hosted runner allowlist to `tahoe` / `tahoe-xl` - `.github/workflows/build.yml` — runner `sequoia-xl` → `tahoe-xl`; Xcode selection updated to `xcodes select 26.3` - `.github/workflows/setup-node-modules.yml` — runner `sequoia-xl` → `tahoe-xl` (must match build consumer for native dep symlinks) - `.github/workflows/build-ios-e2e.yml` — runner `sequoia-xl` → `tahoe-xl` - `.github/workflows/run-e2e-smoke-tests-ios-flask.yml` — runner `sequoia` → `tahoe` - `.github/workflows/run-e2e-workflow.yml` — runner `sequoia` → `tahoe` - `.github/workflows/update-e2e-fixtures.yml` — runner `sequoia` → `tahoe` - `.github/workflows/upload-to-testflight.yml` — runner `sequoia-xl` → `tahoe-xl` ## **Changelog** <!-- If this PR is not End-User-Facing and should not show up in the CHANGELOG, you can choose to either: 1. Write `CHANGELOG entry: null` 2. Label with `no-changelog` --> CHANGELOG entry: null ## **Related issues** Fixes: ## **Manual testing steps** N/A ## **Screenshots/Recordings** ### **Before** N/A ### **After** N/A ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I've included tests if applicable - [x] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I've applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. <!-- Generated with the help of the pr-description AI skill --> Made with [Cursor](https://cursor.com) <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Medium risk because it changes CI runner images and Xcode selection logic across multiple iOS build/E2E workflows; misconfiguration could break iOS builds/tests despite minimal product-code impact. > > **Overview** > **Migrates iOS CI execution to Cirrus Labs `tahoe`/`tahoe-xl` runners** across build, TestFlight upload, E2E, and fixture-update workflows to ensure Xcode 26.x availability. > > **Standardizes Xcode selection** by switching the iOS build workflow to `xcodes select 26.3`. > > **Brings E2E setup in-repo** by adding a composite action `./.github/actions/setup-e2e-env` and updating workflows to use it, and updates Detox’s default iOS simulator from `iPhone 15 Pro` to `iPhone 16 Pro` (plus matching docs). > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 4cea412. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent f520f7e commit bc06cd5

12 files changed

Lines changed: 383 additions & 21 deletions

.detoxrc.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ module.exports = {
8282
'ios.simulator': {
8383
type: 'ios.simulator',
8484
device: {
85-
type: 'iPhone 15 Pro',
85+
type: 'iPhone 16 Pro',
8686
},
8787
},
8888
'android.emulator': {

.github/actionlint.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ self-hosted-runner:
99
- "gha-mm-scale-set-ubuntu-22.04-amd64-small"
1010
- "gha-mm-scale-set-ubuntu-22.04-amd64-med"
1111
- "macos-15"
12-
- "ghcr.io/cirruslabs/macos-runner:sequoia"
13-
- "ghcr.io/cirruslabs/macos-runner:sequoia-xl"
12+
- "ghcr.io/cirruslabs/macos-runner:tahoe"
13+
- "ghcr.io/cirruslabs/macos-runner:tahoe-xl"
1414
- "ghcr.io/cirruslabs/ubuntu-runner-amd64:24.04-md"
1515
- "ghcr.io/cirruslabs/ubuntu-runner-amd64:24.04-lg"
1616
- "ghcr.io/cirruslabs/ubuntu-runner-amd64:24.04-xl"
Lines changed: 362 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,362 @@
1+
name: 'Setup E2E Test Environment'
2+
description: 'Sets up the environment for running E2E tests'
3+
4+
inputs:
5+
platform:
6+
description: 'Platform (ios or android)'
7+
required: true
8+
node-version:
9+
description: 'Node.js version'
10+
required: false
11+
default: '20.18.0'
12+
yarn-version:
13+
description: Yarn version to use with Corepack
14+
required: false
15+
default: '3.8.7'
16+
setup-simulator:
17+
description: 'Whether to setup simulator/emulator'
18+
required: false
19+
default: 'false'
20+
bundler-version:
21+
description: 'Bundler version to use (only for iOS)'
22+
required: false
23+
default: '2.5.8'
24+
cache-prefix:
25+
description: 'Cache key prefix'
26+
required: false
27+
default: 'e2e'
28+
ruby-version:
29+
description: Ruby version to use (only for iOS)
30+
required: false
31+
default: '3.2.9'
32+
xcode-version:
33+
description: 'Xcode version to select (e.g., 26.3). Uses xcodes CLI on Cirrus Labs runners, falls back to xcode-select on GitHub-hosted runners.'
34+
required: false
35+
default: '26.3'
36+
jdk-version:
37+
description: JDK version to use (only for Android)
38+
required: false
39+
default: '17'
40+
jdk-distribution:
41+
description: JDK distribution to use (only for Android)
42+
required: false
43+
default: 'temurin'
44+
foundry-version:
45+
description: Foundry version to install
46+
required: false
47+
default: 'v0.3.0'
48+
android-avd-name:
49+
description: 'Name of AVD to create and boot (for Android)'
50+
required: false
51+
default: 'test_e2e_avd'
52+
android-device:
53+
description: 'AVD device profile (e.g. "pixel_5", "pixel", "Nexus 6")'
54+
required: false
55+
default: 'pixel_5'
56+
android-api-level:
57+
description: 'Android API level to use (e.g. "34")'
58+
required: false
59+
default: '34'
60+
android-abi:
61+
description: 'System architecture ABI for the Android system image (e.g. x86_64, arm64-v8a, armeabi-v7a)'
62+
required: false
63+
default: 'x86_64'
64+
android-tag:
65+
description: 'Android system image tag (e.g. google_apis, default)'
66+
required: false
67+
default: 'google_apis'
68+
android-sdcard-size:
69+
description: 'SD card size for AVD (e.g. 8092M)'
70+
required: false
71+
default: '8092M'
72+
configure-keystores:
73+
description: 'Whether to configure keystores for E2E tests'
74+
required: false
75+
default: 'true'
76+
keystore-role-to-assume:
77+
description: 'AWS IAM role to assume for keystore configuration'
78+
required: false
79+
default: 'arn:aws:iam::363762752069:role/metamask-mobile-build-signer-qa'
80+
target:
81+
description: 'Target for which the keystore is being configured (e.g., qa, flask, main)'
82+
required: false
83+
default: 'qa'
84+
85+
runs:
86+
using: 'composite'
87+
steps:
88+
## Common Setup ##
89+
- run: echo "Setup E2E Environment started"
90+
shell: bash
91+
92+
## Android Setup (early for fail-fast) ##
93+
94+
# Set Android environment variables (self-hosted runner has SDK pre-installed)
95+
- name: Set Android environment variables
96+
if: ${{ inputs.platform == 'android' }}
97+
run: |
98+
echo "ANDROID_HOME=/opt/android-sdk" >> "$GITHUB_ENV"
99+
echo "ANDROID_SDK_ROOT=/opt/android-sdk" >> "$GITHUB_ENV"
100+
shell: bash
101+
102+
- name: Configure Android Signing Certificates
103+
if: ${{ inputs.platform == 'android' && inputs.configure-keystores == 'true' }}
104+
uses: MetaMask/github-tools/.github/actions/configure-keystore@0259e8a920318b02a8860e178d79796eaa08de02
105+
with:
106+
aws-role-to-assume: ${{ inputs.keystore-role-to-assume }}
107+
aws-region: 'us-east-2'
108+
platform: 'android'
109+
target: ${{ inputs.target }}
110+
111+
## JDK Setup
112+
- name: Setup Java
113+
if: ${{ inputs.platform == 'android' }}
114+
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00
115+
with:
116+
java-version: ${{ inputs.jdk-version }}
117+
distribution: ${{ inputs.jdk-distribution }}
118+
119+
- name: Install required emulator dependencies
120+
if: ${{ inputs.platform == 'android' && runner.os == 'Linux' }}
121+
run: |
122+
sudo apt-get update
123+
sudo apt-get install -y \
124+
libpulse0 \
125+
libglu1-mesa \
126+
libnss3 \
127+
libxss1
128+
129+
echo "✅ Linux dependencies installed successfully"
130+
shell: bash
131+
132+
## Android SDK Setup (SDK pre-installed in container)
133+
134+
- name: Install additional Android SDK components if needed
135+
if: ${{ inputs.platform == 'android' && (inputs.android-api-level != '34' || inputs.android-abi != 'x86_64') }}
136+
env:
137+
ANDROID_API_LEVEL: ${{ inputs.android-api-level }}
138+
ANDROID_ABI: ${{ inputs.android-abi }}
139+
run: |
140+
IMAGE="system-images;android-$ANDROID_API_LEVEL;google_apis;$ANDROID_ABI"
141+
echo "Installing additional system image: $IMAGE"
142+
echo "y" | "/opt/android-sdk/cmdline-tools/latest/bin/sdkmanager" "$IMAGE"
143+
shell: bash
144+
145+
## Launch AVD
146+
147+
- name: Set ANDROID_AVD_HOME for downstream steps
148+
if: ${{ inputs.platform == 'android'}}
149+
shell: bash
150+
run: |
151+
echo "ANDROID_AVD_HOME=$HOME/.android/avd" >> "$GITHUB_ENV"
152+
mkdir -p "$HOME/.android/avd"
153+
154+
- name: Create Android Virtual Device (AVD)
155+
if: ${{ inputs.platform == 'android'}}
156+
env:
157+
ANDROID_API_LEVEL: ${{ inputs.android-api-level }}
158+
ANDROID_TAG: ${{ inputs.android-tag }}
159+
ANDROID_ABI: ${{ inputs.android-abi }}
160+
ANDROID_AVD_NAME: ${{ inputs.android-avd-name }}
161+
ANDROID_DEVICE: ${{ inputs.android-device }}
162+
ANDROID_SDCARD_SIZE: ${{ inputs.android-sdcard-size }}
163+
run: |
164+
IMAGE="system-images;android-$ANDROID_API_LEVEL;$ANDROID_TAG;$ANDROID_ABI"
165+
echo "Creating AVD with image: $IMAGE"
166+
"/opt/android-sdk/cmdline-tools/latest/bin/avdmanager" --verbose create avd \
167+
--force \
168+
--name "$ANDROID_AVD_NAME" \
169+
--package "$IMAGE" \
170+
--device "$ANDROID_DEVICE" \
171+
--tag "$ANDROID_TAG" \
172+
--abi "$ANDROID_ABI" \
173+
--sdcard "$ANDROID_SDCARD_SIZE"
174+
shell: bash
175+
176+
## iOS Platform Setup ##
177+
178+
- name: Configure iOS Signing Certificates
179+
if: ${{ inputs.platform == 'ios' && inputs.configure-keystores == 'true' }}
180+
uses: MetaMask/github-tools/.github/actions/configure-keystore@0259e8a920318b02a8860e178d79796eaa08de02
181+
with:
182+
aws-role-to-assume: ${{ inputs.keystore-role-to-assume }}
183+
aws-region: 'us-east-2'
184+
platform: 'ios'
185+
target: ${{ inputs.target }}
186+
187+
## Node.js & JavaScript Dependencies Setup ##
188+
189+
- name: Setup Node.js
190+
uses: actions/setup-node@v6
191+
with:
192+
node-version: ${{ inputs.node-version }}
193+
194+
## Yarn Setup & Cache Management
195+
196+
- name: Get Corepack install command
197+
id: get-corepack-command
198+
env:
199+
YARN_VERSION: ${{ inputs.yarn-version }}
200+
shell: bash
201+
run: |
202+
echo "COREPACK_COMMAND=corepack enable && corepack prepare yarn@$YARN_VERSION --activate" >> "$GITHUB_OUTPUT"
203+
204+
- name: Corepack
205+
id: corepack
206+
uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 #v3.0.2
207+
with:
208+
timeout_minutes: 15
209+
max_attempts: 3
210+
retry_wait_seconds: 30
211+
command: ${{ steps.get-corepack-command.outputs.COREPACK_COMMAND }}
212+
213+
- name: Restore Yarn cache
214+
uses: actions/cache@v4
215+
with:
216+
path: |
217+
node_modules
218+
key: ${{ inputs.cache-prefix }}-yarn-${{ inputs.platform }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
219+
220+
- name: Install JavaScript dependencies with retry
221+
id: yarn-install
222+
uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 #v3.0.2
223+
with:
224+
timeout_minutes: 15
225+
max_attempts: 3
226+
retry_wait_seconds: 30
227+
command: yarn install --immutable
228+
env:
229+
NODE_OPTIONS: --max-old-space-size=4096
230+
YARN_ENABLE_GLOBAL_CACHE: 'true'
231+
232+
- name: Install Foundry
233+
shell: bash
234+
env:
235+
FOUNDRY_VERSION: ${{ inputs.foundry-version }}
236+
run: |
237+
echo "Installing Foundry via foundryup..."
238+
239+
export FOUNDRY_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/.foundry"
240+
export FOUNDRY_BIN="$FOUNDRY_DIR/bin"
241+
242+
mkdir -p "$FOUNDRY_BIN"
243+
244+
curl -sL https://raw.githubusercontent.com/foundry-rs/foundry/master/foundryup/foundryup -o "$FOUNDRY_BIN/foundryup"
245+
chmod +x "$FOUNDRY_BIN/foundryup"
246+
247+
echo "$FOUNDRY_BIN" >> "$GITHUB_PATH"
248+
249+
"$FOUNDRY_BIN/foundryup" -i "$FOUNDRY_VERSION"
250+
251+
## iOS Setup ##
252+
253+
## Ruby Setup & Cache Management
254+
- name: Setup Ruby
255+
if: ${{ inputs.platform == 'ios' }}
256+
uses: ruby/setup-ruby@09a7688d3b55cf0e976497ff046b70949eeaccfd
257+
with:
258+
ruby-version: ${{ inputs.ruby-version }}
259+
260+
- name: Install bundler
261+
if: ${{ inputs.platform == 'ios' }}
262+
run: gem install bundler -v ${{ inputs.bundler-version }}
263+
working-directory: ios
264+
shell: bash
265+
266+
- name: Restore Bundler cache
267+
if: ${{ inputs.platform == 'ios' }}
268+
uses: actions/cache@v4
269+
with:
270+
path: ios/vendor/bundle
271+
key: ${{ inputs.cache-prefix }}-bundler-${{ inputs.platform }}-${{ runner.os }}-${{ hashFiles('ios/Gemfile.lock') }}
272+
restore-keys: |
273+
${{ inputs.cache-prefix }}-bundler-${{ inputs.platform }}-${{ runner.os }}-
274+
275+
- name: Configure bundler install path
276+
if: ${{ inputs.platform == 'ios' }}
277+
run: bundle config set path 'vendor/bundle'
278+
working-directory: ios
279+
shell: bash
280+
281+
- name: Install Ruby gems via bundler
282+
if: ${{ inputs.platform == 'ios' }}
283+
run: bundle install
284+
working-directory: ios
285+
shell: bash
286+
287+
- name: Generate binstubs for CocoaPods
288+
if: ${{ inputs.platform == 'ios' }}
289+
run: bundle binstubs cocoapods --force --path=vendor/bundle/bin
290+
working-directory: ios
291+
shell: bash
292+
293+
- name: Add binstubs to PATH
294+
if: ${{ inputs.platform == 'ios' }}
295+
run: echo "$(pwd)/ios/vendor/bundle/bin" >> "$GITHUB_PATH"
296+
shell: bash
297+
298+
- name: Verify CocoaPods
299+
if: ${{ inputs.platform == 'ios' }}
300+
run: |
301+
bundle show cocoapods || (echo "❌ CocoaPods not installed from ios/Gemfile" && exit 1)
302+
bundle exec pod --version
303+
working-directory: ios
304+
shell: bash
305+
306+
- name: Verify CocoaPods BinStub
307+
if: ${{ inputs.platform == 'ios' }}
308+
run: |
309+
bundle show cocoapods || (echo "❌ CocoaPods not installed from ios/Gemfile" && exit 1)
310+
pod --version
311+
working-directory: ios
312+
shell: bash
313+
314+
- name: Select Xcode ${{ inputs.xcode-version }}
315+
if: ${{ inputs.platform == 'ios' }}
316+
env:
317+
XCODE_VERSION: ${{ inputs.xcode-version }}
318+
run: |
319+
sudo xcodes select "$XCODE_VERSION"
320+
xcodebuild -version
321+
shell: bash
322+
323+
- name: Restore CocoaPods specs cache
324+
if: ${{ inputs.platform == 'ios' }}
325+
id: cocoapods-specs-cache
326+
uses: actions/cache@v4
327+
with:
328+
path: ~/.cocoapods/repos
329+
key: ${{ runner.os }}-cocoapods-specs-${{ hashFiles('ios/Podfile.lock') }}
330+
restore-keys: |
331+
${{ runner.os }}-cocoapods-specs-
332+
continue-on-error: true
333+
334+
- name: Clear CocoaPods trunk to prevent stale specs
335+
if: ${{ inputs.platform == 'ios' }}
336+
run: pod repo remove trunk || true
337+
shell: bash
338+
339+
- name: Install CocoaPods via bundler
340+
if: ${{ inputs.platform == 'ios'}}
341+
uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 #v3.0.2
342+
with:
343+
timeout_minutes: 15
344+
max_attempts: 2
345+
retry_wait_seconds: 30
346+
command: cd ios && bundle exec pod install --repo-update
347+
348+
- name: Install applesimutils
349+
if: ${{ inputs.platform == 'ios' }}
350+
run: |
351+
if ! brew list applesimutils &>/dev/null; then
352+
brew tap wix/brew
353+
brew install applesimutils
354+
else
355+
echo "applesimutils is already installed, skipping..."
356+
fi
357+
shell: bash
358+
359+
- name: Check simutils
360+
if: ${{ inputs.platform == 'ios' }}
361+
run: xcrun simctl list devices
362+
shell: bash

.github/workflows/build-android-e2e.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ jobs:
5858
5959
- name: Setup Android Build Environment
6060
timeout-minutes: 15
61-
uses: MetaMask/github-tools/.github/actions/setup-e2e-env@v1.7.0
61+
uses: ./.github/actions/setup-e2e-env
6262
with:
6363
platform: android
6464
setup-simulator: false

0 commit comments

Comments
 (0)