Skip to content

Commit e37e17b

Browse files
feat: replace unreliable iOS simulator action with custom script
1 parent 4606f20 commit e37e17b

6 files changed

Lines changed: 253 additions & 35 deletions

File tree

.github/CI_DOCUMENTATION.md

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,8 @@ All actions are pinned to specific commit SHAs for security and reproducibility:
7272
| `ruby/setup-ruby` | v1.204.0 | `d697be2f83c6234b20877c3b5eac7a7f342f0d0c` |
7373
| `android-actions/setup-android` | v3.2.1 | `9fc6c4e9069bf8d3d10b2204b1fb8f6ef7065407` |
7474
| `reactivecircus/android-emulator-runner` | v2.33.0 | `b530d96654c385303d652368551fb075bc2f0b6b` |
75-
| `futureware-tech/simulator-action` | v3.1.1 | `dab10d813144ef59b48d401cd95da151222ef8cd` |
75+
76+
> **Note on iOS Simulator:** We use a custom shell script (`.github/scripts/launch-ios-simulator.sh`) instead of `futureware-tech/simulator-action` for better reliability and faster boot times. The action was sometimes unstable and could hang for hours without response.
7677
7778
### Version Consistency Rules
7879

@@ -113,7 +114,7 @@ All actions are pinned to specific commit SHAs for security and reproducibility:
113114
7. **React Native Harness**: Must match between:
114115
- `example/package.json`
115116
- All harness platform packages (`@react-native-harness/*`)
116-
117+
117118
## Workflows
118119

119120
#### 1. Lint (`.github/workflows/lint.yml`)
@@ -159,15 +160,22 @@ runners: [
159160
];
160161
```
161162

162-
The workflows handle device setup automatically using `reactivecircus/android-emulator-runner` and `futureware-tech/simulator-action`, reading device configuration from the harness config.
163+
The workflows handle device setup automatically:
164+
165+
- **Android**: Uses `reactivecircus/android-emulator-runner` to launch the emulator
166+
- **iOS**: Uses a custom script (`.github/scripts/launch-ios-simulator.sh`) that launches the simulator with specific OS version pinning
163167

164168
## Running Tests Locally
165169

166170
### iOS
167171

168-
Make sure `iPhone 17 (iOS 26.0)` simulator is up and running
172+
Make sure `iPhone 17 (iOS 26.0)` simulator is up and running. You can use the launch script:
169173

170174
```bash
175+
# Launch simulator automatically
176+
./.github/scripts/launch-ios-simulator.sh "iPhone 17" "26.0"
177+
178+
# Then run tests
171179
corepack enable
172180
yarn install
173181
yarn prepare
@@ -202,13 +210,14 @@ yarn harness:android:with:build
202210
**iOS simulator not found or fails to boot:**
203211

204212
- Verify simulator model exists in Xcode version (`iPhone 17`)
205-
- Check iOS version compatibility (`26.0`)
206-
- Review simulator-action logs for setup errors
213+
- Check iOS version compatibility (`26.0+`)
214+
- Review simulator launch script logs for errors
207215
- Ensure device name matches harness config exactly
216+
- Check if `jq` is installed (script requires it for JSON parsing)
217+
- Simulator script automatically handles stuck processes and timeouts
208218

209219
## References
210220

211221
- [React Native Harness Documentation](https://www.react-native-harness.dev/)
212222
- [React Native Harness CI/CD Guide](https://www.react-native-harness.dev/docs/guides/ci-cd)
213223
- [Android Emulator Runner Action](https://github.com/ReactiveCircus/android-emulator-runner)
214-
- [iOS Simulator Action](https://github.com/futureware-tech/simulator-action)

.github/actions/setup-node-yarn/action.yml

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,27 @@ runs:
88
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
99
with:
1010
node-version-file: '.nvmrc'
11-
cache: yarn
1211

1312
- name: Enable corepack
1413
shell: bash
1514
run: corepack enable
1615

16+
- name: Get yarn cache directory path
17+
id: yarn-cache-dir-path
18+
shell: bash
19+
run: echo "dir=$(yarn config get cacheFolder)" >> $GITHUB_OUTPUT
20+
21+
- name: Cache Yarn dependencies
22+
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830
23+
with:
24+
path: |
25+
${{ steps.yarn-cache-dir-path.outputs.dir }}
26+
.yarn/cache
27+
.yarn/install-state.gz
28+
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
29+
restore-keys: |
30+
${{ runner.os }}-yarn-
31+
1732
- name: Install dependencies
1833
shell: bash
1934
run: yarn install --immutable
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
#!/bin/bash
2+
3+
# Script to reliably launch iOS Simulator
4+
# Usage: ./launch-ios-simulator.sh <device_name> <os_version> [timeout_seconds]
5+
#
6+
# Examples:
7+
# ./launch-ios-simulator.sh "iPhone 17" "26.0"
8+
# ./launch-ios-simulator.sh "iPhone 17" "26.0" 120
9+
#
10+
# Exit codes:
11+
# 0 - Success
12+
# 1 - Device not found
13+
# 2 - Boot timeout
14+
# 3 - Invalid arguments
15+
16+
set -euo pipefail
17+
18+
# Configuration
19+
DEVICE_NAME="${1:-}"
20+
OS_VERSION="${2:-}"
21+
TIMEOUT="${3:-120}"
22+
CHECK_INTERVAL=2
23+
24+
# Colors for output
25+
RED='\033[0;31m'
26+
GREEN='\033[0;32m'
27+
YELLOW='\033[1;33m'
28+
BLUE='\033[0;34m'
29+
NC='\033[0m' # No Color
30+
31+
# Helper functions
32+
log_info() {
33+
printf "${BLUE}${NC} %s\n" "$1"
34+
}
35+
36+
log_success() {
37+
printf "${GREEN}${NC} %s\n" "$1"
38+
}
39+
40+
log_warning() {
41+
printf "${YELLOW}${NC} %s\n" "$1"
42+
}
43+
44+
log_error() {
45+
printf "${RED}${NC} %s\n" "$1" >&2
46+
}
47+
48+
# Validate arguments
49+
if [ -z "$DEVICE_NAME" ] || [ -z "$OS_VERSION" ]; then
50+
log_error "Usage: $0 <device_name> <os_version> [timeout_seconds]"
51+
log_error "Example: $0 \"iPhone 17\" \"26.0\" 120"
52+
exit 3
53+
fi
54+
55+
log_info "Starting iOS Simulator setup..."
56+
log_info "Device: $DEVICE_NAME"
57+
log_info "OS Version: $OS_VERSION"
58+
log_info "Timeout: ${TIMEOUT}s"
59+
60+
# Kill any stuck simulators
61+
log_info "Checking for existing Simulator processes..."
62+
if pgrep -x "Simulator" > /dev/null; then
63+
log_warning "Found existing Simulator process, shutting down..."
64+
killall Simulator 2>/dev/null || true
65+
sleep 2
66+
fi
67+
68+
# Find the device UDID
69+
log_info "Finding device UDID for '$DEVICE_NAME' with iOS $OS_VERSION..."
70+
71+
# Convert version like "26.0" to runtime key format "iOS-26-0"
72+
IOS_RUNTIME_KEY=$(echo "$OS_VERSION" | sed 's/\./-/g')
73+
74+
# Always show available devices
75+
log_info ""
76+
log_info "Available devices:"
77+
xcrun simctl list devices available --json | \
78+
jq -r '.devices | to_entries[] | select(.key | contains("iOS")) | .key as $runtime | .value[] |
79+
($runtime | capture("iOS-(?<major>[0-9]+)-(?<minor>[0-9]+)") | "iOS \(.major).\(.minor)") as $version |
80+
" \(.name) - \($version) - \(.udid)"'
81+
log_info ""
82+
83+
# Search for device in the specific iOS version runtime
84+
DEVICE_UDID=$(xcrun simctl list devices available --json | \
85+
jq -r --arg name "$DEVICE_NAME" --arg runtime_suffix "iOS-${IOS_RUNTIME_KEY}" \
86+
'.devices | to_entries[] | select(.key | endswith($runtime_suffix)) | .value[] | select(.name == $name and .isAvailable == true) | .udid' | \
87+
head -n 1)
88+
89+
if [ -z "$DEVICE_UDID" ]; then
90+
log_error "Device '$DEVICE_NAME' with iOS $OS_VERSION not found or not available"
91+
log_info "Looking for runtime matching: iOS-${IOS_RUNTIME_KEY}"
92+
exit 1
93+
fi
94+
95+
log_success "Found device: $DEVICE_NAME (UDID: $DEVICE_UDID)"
96+
97+
# Check current state
98+
CURRENT_STATE=$(xcrun simctl list devices --json | \
99+
jq -r --arg udid "$DEVICE_UDID" \
100+
'.devices[] | .[] | select(.udid == $udid) | .state')
101+
102+
log_info "Current state: $CURRENT_STATE"
103+
104+
# Shutdown if already booted to ensure clean state
105+
if [ "$CURRENT_STATE" = "Booted" ]; then
106+
log_warning "Device already booted, shutting down for clean start..."
107+
xcrun simctl shutdown "$DEVICE_UDID" 2>/dev/null || true
108+
sleep 2
109+
fi
110+
111+
# Boot the simulator
112+
log_info "Booting simulator..."
113+
xcrun simctl boot "$DEVICE_UDID" 2>/dev/null || {
114+
# Check if it's already booting or booted
115+
CURRENT_STATE=$(xcrun simctl list devices --json | \
116+
jq -r --arg udid "$DEVICE_UDID" \
117+
'.devices[] | .[] | select(.udid == $udid) | .state')
118+
119+
if [ "$CURRENT_STATE" != "Booted" ] && [ "$CURRENT_STATE" != "Booting" ]; then
120+
log_error "Failed to boot simulator"
121+
exit 2
122+
fi
123+
}
124+
125+
# Wait for simulator to boot
126+
log_info "Waiting for simulator to boot (timeout: ${TIMEOUT}s)..."
127+
ELAPSED=0
128+
while [ $ELAPSED -lt $TIMEOUT ]; do
129+
STATE=$(xcrun simctl list devices --json | \
130+
jq -r --arg udid "$DEVICE_UDID" \
131+
'.devices[] | .[] | select(.udid == $udid) | .state')
132+
133+
if [ "$STATE" = "Booted" ]; then
134+
log_success "Simulator booted successfully!"
135+
break
136+
fi
137+
138+
if [ $((ELAPSED % 10)) -eq 0 ] && [ $ELAPSED -gt 0 ]; then
139+
log_info "Still waiting... (${ELAPSED}s elapsed)"
140+
fi
141+
142+
sleep $CHECK_INTERVAL
143+
ELAPSED=$((ELAPSED + CHECK_INTERVAL))
144+
done
145+
146+
# Verify boot completed
147+
FINAL_STATE=$(xcrun simctl list devices --json | \
148+
jq -r --arg udid "$DEVICE_UDID" \
149+
'.devices[] | .[] | select(.udid == $udid) | .state')
150+
151+
if [ "$FINAL_STATE" != "Booted" ]; then
152+
log_error "Simulator boot timeout after ${TIMEOUT}s (state: $FINAL_STATE)"
153+
exit 2
154+
fi
155+
156+
# Open Simulator.app (optional, for visibility)
157+
log_info "Opening Simulator.app..."
158+
open -a Simulator --args -CurrentDeviceUDID "$DEVICE_UDID" 2>/dev/null || {
159+
log_warning "Failed to open Simulator.app (non-critical)"
160+
}
161+
162+
# Wait for SpringBoard to be ready (optional check, may not work on all systems)
163+
log_info "Waiting for SpringBoard to be ready..."
164+
ELAPSED=0
165+
SPRINGBOARD_READY=false
166+
while [ $ELAPSED -lt 10 ]; do
167+
if xcrun simctl spawn "$DEVICE_UDID" launchctl print system 2>/dev/null | grep -q "com.apple.springboard" 2>/dev/null; then
168+
SPRINGBOARD_READY=true
169+
break
170+
fi
171+
sleep 1
172+
ELAPSED=$((ELAPSED + 1))
173+
done
174+
175+
if [ "$SPRINGBOARD_READY" = true ]; then
176+
log_success "SpringBoard is ready!"
177+
else
178+
log_warning "SpringBoard check timed out (non-critical, simulator may still be usable)"
179+
fi
180+
181+
# Final verification
182+
log_info "Verifying simulator status..."
183+
xcrun simctl bootstatus "$DEVICE_UDID" -b 2>/dev/null || {
184+
log_warning "bootstatus check failed (non-critical)"
185+
}
186+
187+
# Print simulator info
188+
log_success "Simulator is ready!"
189+
echo ""
190+
echo "Device Information:"
191+
echo " Name: $DEVICE_NAME"
192+
echo " OS Version: iOS $OS_VERSION"
193+
echo " UDID: $DEVICE_UDID"
194+
echo " State: $(xcrun simctl list devices --json | jq -r --arg udid "$DEVICE_UDID" '.devices[] | .[] | select(.udid == $udid) | .state')"
195+
echo ""
196+
197+
# Export UDID for use in subsequent steps
198+
echo "SIMULATOR_UDID=$DEVICE_UDID" >> "${GITHUB_ENV:-/dev/null}"
199+
log_success "Simulator UDID exported to GITHUB_ENV"
200+
201+
log_success "Setup complete! Simulator is ready for testing."
202+
exit 0

.github/workflows/ios.yml

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -66,14 +66,15 @@ jobs:
6666
run: yarn app:package:ios
6767

6868
# Setup simulator (after build)
69-
- name: Setup iOS Simulator
70-
uses: futureware-tech/simulator-action@dab10d813144ef59b48d401cd95da151222ef8cd
71-
with:
72-
model: ${{ env.DEVICE_MODEL }}
73-
os: iOS
74-
os_version: ${{ env.IOS_VERSION }}
75-
wait_for_boot: true
76-
erase_before_boot: false
69+
- name: Launch iOS Simulator
70+
run: |
71+
# Install jq if not available
72+
if ! command -v jq &> /dev/null; then
73+
brew install jq
74+
fi
75+
76+
# Launch simulator with our custom script
77+
bash .github/scripts/launch-ios-simulator.sh "${{ env.DEVICE_MODEL }}" "${{ env.IOS_VERSION }}" 120
7778
7879
# Install app on simulator
7980
- name: Install app on simulator

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# Mendix Native
22

3+
[![CI](https://github.com/mendix/mendix-native/actions/workflows/ci.yml/badge.svg)](https://github.com/mendix/mendix-native/actions/workflows/ci.yml)
4+
[![Lint](https://github.com/mendix/mendix-native/actions/workflows/lint.yml/badge.svg)](https://github.com/mendix/mendix-native/actions/workflows/lint.yml)
5+
[![Android](https://github.com/mendix/mendix-native/actions/workflows/android.yml/badge.svg)](https://github.com/mendix/mendix-native/actions/workflows/android.yml)
6+
[![iOS](https://github.com/mendix/mendix-native/actions/workflows/ios.yml/badge.svg)](https://github.com/mendix/mendix-native/actions/workflows/ios.yml)
7+
38
Mendix native mobile package for React Native applications.
49

510
## Prerequisites
@@ -15,6 +20,7 @@ Before you begin, ensure you have the following installed:
1520
## Local Development Setup
1621

1722
This project is a monorepo managed using Yarn workspaces with:
23+
1824
- The library package in the root directory
1925
- An example app in the `example/` directory
2026

@@ -80,10 +86,12 @@ yarn example android
8086
#### Edit Native Code
8187

8288
**iOS (Objective-C/Swift):**
89+
8390
- Open `example/ios/MendixNativeExample.xcworkspace` in Xcode
8491
- Find source files at: `Pods > Development Pods > mendix-native`
8592

8693
**Android (Java/Kotlin):**
94+
8795
- Open `example/android` in Android Studio
8896
- Find source files under: `mendix-native` in the Android view
8997

@@ -231,6 +239,7 @@ Example:
231239
## [Unreleased]
232240

233241
## [1.2.0] - 2025-01-15
242+
234243
<previous unreleased notes>
235244
```
236245

example/__tests__/file-system.harness.ts

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,4 @@
1-
import {
2-
describe,
3-
test,
4-
expect,
5-
beforeEach,
6-
afterEach,
7-
} from 'react-native-harness';
1+
import { describe, test, expect, beforeEach } from 'react-native-harness';
82
import { NativeFileSystem } from 'mendix-native';
93

104
describe('NativeFileSystem', () => {
@@ -24,18 +18,6 @@ describe('NativeFileSystem', () => {
2418
}
2519
});
2620

27-
afterEach(async () => {
28-
// Clean up test files after each test
29-
try {
30-
const exists = await NativeFileSystem.fileExists(testFilePath);
31-
if (exists) {
32-
await NativeFileSystem.remove(testFilePath);
33-
}
34-
} catch (error) {
35-
// Ignore cleanup errors
36-
}
37-
});
38-
3921
describe('Constants', () => {
4022
test('should have DocumentDirectoryPath defined', () => {
4123
const documentDirectory = NativeFileSystem.DocumentDirectoryPath;

0 commit comments

Comments
 (0)