From c0b0336038f3ca0b92775f8bd8582003381ea7d1 Mon Sep 17 00:00:00 2001 From: Alex Freas Date: Tue, 5 May 2026 18:52:13 +0200 Subject: [PATCH 1/7] [NT-3070] Wire ios-sdk into the pnpm implementation runner with build and run scripts --- implementations/ios-sdk/package.json | 10 ++++++++++ package.json | 3 +++ 2 files changed, 13 insertions(+) create mode 100644 implementations/ios-sdk/package.json diff --git a/implementations/ios-sdk/package.json b/implementations/ios-sdk/package.json new file mode 100644 index 00000000..389195c6 --- /dev/null +++ b/implementations/ios-sdk/package.json @@ -0,0 +1,10 @@ +{ + "name": "@implementation/ios-sdk", + "version": "0.0.0", + "private": true, + "scripts": { + "xcodegen": "xcodegen generate", + "test:e2e:ios:build:release": "pnpm run xcodegen && xcodebuild build-for-testing -project OptimizationApp.xcodeproj -scheme OptimizationAppSwiftUI -configuration Release -destination 'generic/platform=iOS Simulator' -derivedDataPath /tmp/optimization-ios-derived-data CODE_SIGNING_ALLOWED=NO COMPILER_INDEX_STORE_ENABLE=NO | xcbeautify && xcodebuild build-for-testing -project OptimizationApp.xcodeproj -scheme OptimizationAppUIKit -configuration Release -destination 'generic/platform=iOS Simulator' -derivedDataPath /tmp/optimization-ios-derived-data CODE_SIGNING_ALLOWED=NO COMPILER_INDEX_STORE_ENABLE=NO | xcbeautify", + "test:e2e:ios:run:release": "xcodebuild test-without-building -xctestrun \"$(ls /tmp/optimization-ios-derived-data/Build/Products/OptimizationAppUITests${IOS_SCHEME}_*.xctestrun | head -1)\" -destination \"platform=iOS Simulator,name=${IOS_SIM_NAME:-iPhone 16},OS=${IOS_SIM_OS:-latest}\" -resultBundlePath /tmp/optimization-ios-derived-data/Test-${IOS_SCHEME}.xcresult ${IOS_ONLY_TESTING:+-only-testing:${IOS_ONLY_TESTING}} | xcbeautify" + } +} diff --git a/package.json b/package.json index a3802b29..133358a0 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "format:check": "prettier . --check", "format:fix": "prettier . --check --write", "implementation:install": "pnpm run build:pkgs && pnpm run implementation:run -- --all -- implementation:install", + "implementation:ios-sdk": "pnpm run implementation:run -- ios-sdk", "implementation:node-sdk": "pnpm run implementation:run -- node-sdk", "implementation:node-sdk+web-sdk": "pnpm run implementation:run -- node-sdk+web-sdk", "implementation:react-native-sdk": "pnpm run implementation:run -- react-native-sdk", @@ -37,6 +38,7 @@ "prepare": "husky", "serve:mocks": "pnpm --dir lib/mocks serve", "setup:e2e": "pnpm run build:pkgs && pnpm run implementation:run -- --all -- implementation:install && pnpm run playwright:install && pnpm run playwright:install-deps", + "setup:e2e:ios-sdk": "pnpm run implementation:ios-sdk -- xcodegen", "setup:e2e:node-sdk": "pnpm run build:pkgs && pnpm run implementation:run -- node-sdk implementation:install && pnpm run implementation:run -- node-sdk implementation:setup:e2e", "setup:e2e:node-sdk+web-sdk": "pnpm run build:pkgs && pnpm run implementation:run -- node-sdk+web-sdk implementation:install && pnpm run implementation:run -- node-sdk+web-sdk implementation:setup:e2e", "setup:e2e:react-native-sdk": "pnpm run build:pkgs && pnpm run implementation:run -- react-native-sdk implementation:install && pnpm run implementation:run -- react-native-sdk implementation:setup:e2e", @@ -44,6 +46,7 @@ "setup:e2e:web-sdk_react": "pnpm run build:pkgs && pnpm run implementation:run -- web-sdk_react implementation:install && pnpm run implementation:run -- web-sdk_react implementation:setup:e2e", "setup:e2e:web-sdk": "pnpm run build:pkgs && pnpm run implementation:run -- web-sdk implementation:install && pnpm run implementation:run -- web-sdk implementation:setup:e2e", "test:e2e": "pnpm run setup:e2e && pnpm run implementation:run -- --all -- implementation:test:e2e:run", + "test:e2e:ios-sdk": "pnpm run setup:e2e:ios-sdk && pnpm run implementation:ios-sdk -- test:e2e:ios:build:release && IOS_SCHEME=SwiftUI pnpm run implementation:ios-sdk -- test:e2e:ios:run:release && IOS_SCHEME=UIKit pnpm run implementation:ios-sdk -- test:e2e:ios:run:release", "test:e2e:node-sdk": "pnpm run setup:e2e:node-sdk && pnpm run implementation:run -- node-sdk implementation:test:e2e:run", "test:e2e:node-sdk+web-sdk": "pnpm run setup:e2e:node-sdk+web-sdk && pnpm run implementation:run -- node-sdk+web-sdk implementation:test:e2e:run", "test:e2e:react-native-sdk": "pnpm run setup:e2e:react-native-sdk && pnpm run implementation:run -- react-native-sdk implementation:test:e2e:run", From cba3e7c6cb263a83e889dd8f37781bdd29936174 Mon Sep 17 00:00:00 2001 From: Alex Freas Date: Tue, 5 May 2026 18:54:14 +0200 Subject: [PATCH 2/7] [NT-3070] Add iOS UI test build and matrix run jobs to the main pipeline --- .github/workflows/main-pipeline.yaml | 151 +++++++++++++++++++++++++++ 1 file changed, 151 insertions(+) diff --git a/.github/workflows/main-pipeline.yaml b/.github/workflows/main-pipeline.yaml index 99cf5ae8..9094379c 100644 --- a/.github/workflows/main-pipeline.yaml +++ b/.github/workflows/main-pipeline.yaml @@ -26,6 +26,7 @@ jobs: e2e_web_sdk_react: ${{ steps.filter.outputs.e2e_web_sdk_react }} e2e_react_web_sdk: ${{ steps.filter.outputs.e2e_react_web_sdk }} e2e_react_native_android: ${{ steps.filter.outputs.e2e_react_native_android }} + e2e_ios: ${{ steps.filter.outputs.e2e_ios }} steps: - uses: namespacelabs/nscloud-checkout-action@938f5d2d403d6224d9a0c0dc559b1dae09c2ede4 # v8.1.1 @@ -125,6 +126,14 @@ jobs: - 'package.json' - 'pnpm-lock.yaml' - '.github/workflows/main-pipeline.yaml' + # iOS native implementation E2E coverage scope. + e2e_ios: + - 'implementations/ios-sdk/**' + - 'lib/mocks/**' + - 'packages/ios/**' + - 'package.json' + - 'pnpm-lock.yaml' + - '.github/workflows/main-pipeline.yaml' setup: name: 🛠️ pnpm install @@ -650,6 +659,57 @@ jobs: if-no-files-found: error retention-days: 1 + e2e-ios-sdk-build: + name: 🍎 Build iOS UI Test Bundles + runs-on: nscloud-macos-arm64-8x32 + timeout-minutes: 30 + needs: [setup, changes] + if: needs.changes.outputs.e2e_ios == 'true' + env: + DERIVED_DATA: /tmp/optimization-ios-derived-data + steps: + - uses: namespacelabs/nscloud-checkout-action@938f5d2d403d6224d9a0c0dc559b1dae09c2ede4 # v8.1.1 + + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version-file: '.nvmrc' + package-manager-cache: false + + - uses: pnpm/action-setup@903f9c1a6ebcba6cf41d87230be49611ac97822e # v6.0.3 + + - name: Install XcodeGen and xcbeautify + run: brew install xcodegen xcbeautify + + - name: Set up caches (Namespace) + uses: namespacelabs/nscloud-cache-action@15799a6b54e5765f85b2aac25b3f0df43ed571c0 # v1.4.3 + with: + cache: pnpm + path: | + ~/Library/Caches/org.swift.swiftpm + + - name: Show toolchain + run: | + xcodebuild -version + xcrun simctl list runtimes | head + + - run: pnpm install --prefer-offline --frozen-lockfile + + - name: Build iOS UI test bundles (SwiftUI + UIKit) + run: pnpm run implementation:ios-sdk -- test:e2e:ios:build:release + + - name: Stage Build/Products for artifact + run: | + mkdir -p /tmp/ios-artifact + cp -R "$DERIVED_DATA/Build/Products" /tmp/ios-artifact/Products + ls /tmp/ios-artifact/Products/*.xctestrun + + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: ios-uitest-bundles + path: /tmp/ios-artifact/ + if-no-files-found: error + retention-days: 1 + e2e-react-native-android: name: 📱 E2E React Native Android (shard ${{ matrix.shard }}/2) runs-on: namespace-profile-linux-16-vcpu-32-gb-ram-optimal @@ -808,3 +868,94 @@ jobs: implementations/react-native-sdk/.detox/ /tmp/mock-server.log retention-days: 7 + + e2e-ios-sdk: + name: 🍎 E2E iOS UI (${{ matrix.scheme }}) + runs-on: nscloud-macos-arm64-8x32 + timeout-minutes: 45 + needs: [setup, changes, e2e-ios-sdk-build] + if: needs.changes.outputs.e2e_ios == 'true' + strategy: + fail-fast: false + matrix: + include: + - scheme: SwiftUI + - scheme: UIKit + env: + DERIVED_DATA: /tmp/optimization-ios-derived-data + IOS_SCHEME: ${{ matrix.scheme }} + IOS_SIM_NAME: 'iPhone 16' + IOS_SIM_OS: 'latest' + # Smoke mode for the first PR — restrict to one test class per scheme. + # Remove this env var in a follow-up PR to enable the full suite. + IOS_ONLY_TESTING: OptimizationAppUITests${{ matrix.scheme }}/PreviewPanelTests + steps: + - uses: namespacelabs/nscloud-checkout-action@938f5d2d403d6224d9a0c0dc559b1dae09c2ede4 # v8.1.1 + + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version-file: '.nvmrc' + package-manager-cache: false + + - uses: pnpm/action-setup@903f9c1a6ebcba6cf41d87230be49611ac97822e # v6.0.3 + + - name: Install xcbeautify + run: brew install xcbeautify + + - uses: namespacelabs/nscloud-cache-action@15799a6b54e5765f85b2aac25b3f0df43ed571c0 # v1.4.3 + with: + cache: pnpm + + - run: pnpm install --prefer-offline --frozen-lockfile + + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: ios-uitest-bundles + path: /tmp/ios-artifact/ + + - name: Reconstruct DerivedData layout at stable path + run: | + mkdir -p "$DERIVED_DATA/Build" + mv /tmp/ios-artifact/Products "$DERIVED_DATA/Build/Products" + ls "$DERIVED_DATA/Build/Products" + + - name: Boot iOS Simulator + run: | + DEVICE_UDID=$(xcrun simctl create "ci-${{ matrix.scheme }}" "$IOS_SIM_NAME") + echo "DEVICE_UDID=$DEVICE_UDID" >> "$GITHUB_ENV" + xcrun simctl boot "$DEVICE_UDID" + xcrun simctl bootstatus "$DEVICE_UDID" -b + + - name: Start Mock Server + run: | + pnpm --dir lib/mocks serve > /tmp/mock-server.log 2>&1 & + echo $! > /tmp/mock-server.pid + for i in {1..60}; do + if nc -z localhost 8000 2>/dev/null; then + echo "Mock server is ready" + break + fi + echo "Waiting for mock server... ($i/60)" + sleep 1 + done + if ! nc -z localhost 8000 2>/dev/null; then + echo "Mock server failed to start:" + cat /tmp/mock-server.log + exit 1 + fi + + - name: Run iOS UI tests (${{ matrix.scheme }}) + run: pnpm run implementation:ios-sdk -- test:e2e:ios:run:release + + - name: Stop Mock Server + if: always() + run: kill $(cat /tmp/mock-server.pid) 2>/dev/null || true + + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + if: ${{ !cancelled() }} + with: + name: ci-results-ios-${{ matrix.scheme }} + path: | + /tmp/optimization-ios-derived-data/Test-*.xcresult + /tmp/mock-server.log + retention-days: 7 From b2199535bcced80c4176d154209b91bcc1c1041f Mon Sep 17 00:00:00 2001 From: Alex Freas Date: Wed, 6 May 2026 15:41:17 +0200 Subject: [PATCH 3/7] Update runs-on to namespace-profile-macos-apple-silicon-arm64-6-cpu-14-gb for ios ui tests --- .github/workflows/main-pipeline.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main-pipeline.yaml b/.github/workflows/main-pipeline.yaml index 9094379c..7c93d897 100644 --- a/.github/workflows/main-pipeline.yaml +++ b/.github/workflows/main-pipeline.yaml @@ -661,7 +661,7 @@ jobs: e2e-ios-sdk-build: name: 🍎 Build iOS UI Test Bundles - runs-on: nscloud-macos-arm64-8x32 + runs-on: namespace-profile-macos-apple-silicon-arm64-6-cpu-14-gb timeout-minutes: 30 needs: [setup, changes] if: needs.changes.outputs.e2e_ios == 'true' From 5ab14dc0146a418aa69d372243b0c21f5dd53eb8 Mon Sep 17 00:00:00 2001 From: Alex Freas Date: Thu, 7 May 2026 15:02:00 +0200 Subject: [PATCH 4/7] Update runs-on to namespace-profile-macos-apple-silicon-arm64-6-cpu-14-gb for ios ui tests for e2e root --- .github/workflows/main-pipeline.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main-pipeline.yaml b/.github/workflows/main-pipeline.yaml index 7c93d897..644a479d 100644 --- a/.github/workflows/main-pipeline.yaml +++ b/.github/workflows/main-pipeline.yaml @@ -871,7 +871,7 @@ jobs: e2e-ios-sdk: name: 🍎 E2E iOS UI (${{ matrix.scheme }}) - runs-on: nscloud-macos-arm64-8x32 + runs-on: namespace-profile-macos-apple-silicon-arm64-6-cpu-14-gb timeout-minutes: 45 needs: [setup, changes, e2e-ios-sdk-build] if: needs.changes.outputs.e2e_ios == 'true' From 986dfc443ee7198da06a1de3b1515f7b16e1dcd1 Mon Sep 17 00:00:00 2001 From: Alex Freas Date: Thu, 7 May 2026 16:28:51 +0200 Subject: [PATCH 5/7] [NT-3070] Fix iOS UI test runner xctestrun glob and propagate pipe exit code --- implementations/ios-sdk/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/implementations/ios-sdk/package.json b/implementations/ios-sdk/package.json index 389195c6..ca2079a9 100644 --- a/implementations/ios-sdk/package.json +++ b/implementations/ios-sdk/package.json @@ -4,7 +4,7 @@ "private": true, "scripts": { "xcodegen": "xcodegen generate", - "test:e2e:ios:build:release": "pnpm run xcodegen && xcodebuild build-for-testing -project OptimizationApp.xcodeproj -scheme OptimizationAppSwiftUI -configuration Release -destination 'generic/platform=iOS Simulator' -derivedDataPath /tmp/optimization-ios-derived-data CODE_SIGNING_ALLOWED=NO COMPILER_INDEX_STORE_ENABLE=NO | xcbeautify && xcodebuild build-for-testing -project OptimizationApp.xcodeproj -scheme OptimizationAppUIKit -configuration Release -destination 'generic/platform=iOS Simulator' -derivedDataPath /tmp/optimization-ios-derived-data CODE_SIGNING_ALLOWED=NO COMPILER_INDEX_STORE_ENABLE=NO | xcbeautify", - "test:e2e:ios:run:release": "xcodebuild test-without-building -xctestrun \"$(ls /tmp/optimization-ios-derived-data/Build/Products/OptimizationAppUITests${IOS_SCHEME}_*.xctestrun | head -1)\" -destination \"platform=iOS Simulator,name=${IOS_SIM_NAME:-iPhone 16},OS=${IOS_SIM_OS:-latest}\" -resultBundlePath /tmp/optimization-ios-derived-data/Test-${IOS_SCHEME}.xcresult ${IOS_ONLY_TESTING:+-only-testing:${IOS_ONLY_TESTING}} | xcbeautify" + "test:e2e:ios:build:release": "set -o pipefail && pnpm run xcodegen && xcodebuild build-for-testing -project OptimizationApp.xcodeproj -scheme OptimizationAppSwiftUI -configuration Release -destination 'generic/platform=iOS Simulator' -derivedDataPath /tmp/optimization-ios-derived-data CODE_SIGNING_ALLOWED=NO COMPILER_INDEX_STORE_ENABLE=NO | xcbeautify && xcodebuild build-for-testing -project OptimizationApp.xcodeproj -scheme OptimizationAppUIKit -configuration Release -destination 'generic/platform=iOS Simulator' -derivedDataPath /tmp/optimization-ios-derived-data CODE_SIGNING_ALLOWED=NO COMPILER_INDEX_STORE_ENABLE=NO | xcbeautify", + "test:e2e:ios:run:release": "set -o pipefail && xcodebuild test-without-building -xctestrun \"$(ls /tmp/optimization-ios-derived-data/Build/Products/OptimizationApp${IOS_SCHEME}_*.xctestrun | head -1)\" -destination \"platform=iOS Simulator,name=${IOS_SIM_NAME:-iPhone 16},OS=${IOS_SIM_OS:-latest}\" -resultBundlePath /tmp/optimization-ios-derived-data/Test-${IOS_SCHEME}.xcresult ${IOS_ONLY_TESTING:+-only-testing:${IOS_ONLY_TESTING}} | xcbeautify" } } From 4d2ae1c388c45ac0fa720ccd782bc3b9a5617b3b Mon Sep 17 00:00:00 2001 From: Alex Freas Date: Thu, 7 May 2026 16:28:58 +0200 Subject: [PATCH 6/7] [NT-3070] Assert iOS xctestrun exists and enable pipefail in CI test step --- .github/workflows/main-pipeline.yaml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.github/workflows/main-pipeline.yaml b/.github/workflows/main-pipeline.yaml index 644a479d..9b0db815 100644 --- a/.github/workflows/main-pipeline.yaml +++ b/.github/workflows/main-pipeline.yaml @@ -944,7 +944,20 @@ jobs: exit 1 fi + - name: Verify built xctestrun exists for ${{ matrix.scheme }} + shell: 'bash -eo pipefail {0}' + run: | + shopt -s nullglob + matches=("$DERIVED_DATA"/Build/Products/OptimizationApp"$IOS_SCHEME"_*.xctestrun) + if [ ${#matches[@]} -eq 0 ]; then + echo "No xctestrun found for scheme $IOS_SCHEME under $DERIVED_DATA/Build/Products/" >&2 + ls -la "$DERIVED_DATA/Build/Products/" || true + exit 1 + fi + printf 'Found xctestrun: %s\n' "${matches[@]}" + - name: Run iOS UI tests (${{ matrix.scheme }}) + shell: 'bash -eo pipefail {0}' run: pnpm run implementation:ios-sdk -- test:e2e:ios:run:release - name: Stop Mock Server From b4d6cc9fc9ade9828ad8c554d2f9a68aeed0a10a Mon Sep 17 00:00:00 2001 From: Alex Freas Date: Thu, 7 May 2026 16:36:19 +0200 Subject: [PATCH 7/7] [NT-3070] Make weak JSContext bindings mutable to satisfy the Swift compiler --- .../ContentfulOptimization/Polyfills/NativePolyfills.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ios/ContentfulOptimization/Sources/ContentfulOptimization/Polyfills/NativePolyfills.swift b/packages/ios/ContentfulOptimization/Sources/ContentfulOptimization/Polyfills/NativePolyfills.swift index a3ae7b91..f8626d73 100644 --- a/packages/ios/ContentfulOptimization/Sources/ContentfulOptimization/Polyfills/NativePolyfills.swift +++ b/packages/ios/ContentfulOptimization/Sources/ContentfulOptimization/Polyfills/NativePolyfills.swift @@ -76,7 +76,7 @@ enum NativePolyfills { } private static func registerNativeSetTimeout(in context: JSContext, timerStore: TimerStore) { - weak let weakContext = context + weak var weakContext = context let nativeSetTimeout: @convention(block) (Int, Int) -> Void = { timerId, delayMs in let workItem = DispatchWorkItem { guard let ctx = weakContext else { return } @@ -107,7 +107,7 @@ enum NativePolyfills { } private static func registerNativeFetch(in context: JSContext) { - weak let weakContext = context + weak var weakContext = context let nativeFetch: @convention(block) (String, String, String, JSValue, Int) -> Void = { urlString, method, headersJSON, bodyValue, callbackId in