Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
69 commits
Select commit Hold shift + click to select a range
95d6afc
Test hardening, bug fixes on iOS, RN, and Android. Add CI testing for…
akfreas May 22, 2026
48f103a
Consolidate the duplicated iOS and Android JS bridge packages into a …
akfreas May 22, 2026
0144b89
Repoint the iOS build script and the iOS and Android SDK docs at the …
akfreas May 22, 2026
fd8665d
Restore the Android JS bridge bundle to its minified build output, re…
akfreas May 22, 2026
46a89c2
Port the getMergeTagValue and flag bridge methods into the merged opt…
akfreas May 22, 2026
6a59652
prevent CI from silently passing when Android instrumentation runner …
akfreas May 25, 2026
13a21e1
Add a com.contentful.optimization.views adapter mirroring the Compose…
akfreas May 25, 2026
c852ee2
Extract the platform-agnostic app helpers into a new :shared Kotlin l…
akfreas May 25, 2026
f11a644
Rename the Android Compose reference impl module from :app to :compos…
akfreas May 25, 2026
a181512
Add the views/ XML Views reference implementation mirroring the Compo…
akfreas May 25, 2026
d2af545
Read the APP_PACKAGE for AppLauncher from the instrumentation runner …
akfreas May 25, 2026
355dbf8
Add per-app pnpm scripts (test:e2e:compose, test:e2e:views, build:vie…
akfreas May 25, 2026
8de7544
Split the Android E2E job into a build-once stage that produces the c…
akfreas May 25, 2026
2d6b70b
Document the dual-impl Android reference layout, the views/compose te…
akfreas May 25, 2026
3d754c6
Fix three parity gaps in the Views reference impl uncovered by runnin…
akfreas May 25, 2026
bca9550
Move SDK initialization from ViewsApplication.onCreate into MainActiv…
akfreas May 25, 2026
c85619b
Wait for the SDK's first non-null selectedPersonalizations emission i…
akfreas May 25, 2026
75de52c
Use a workspace-relative android-apks/ directory for the build->matri…
akfreas May 25, 2026
63b91e9
Drop the manual adb shell settings put global animation-scale lines f…
akfreas May 25, 2026
78f1e14
Drop set -o pipefail from the emulator-runner script — the action inv…
akfreas May 25, 2026
90c01c8
Belt-and-suspenders: surface the test tag through contentDescription …
akfreas May 25, 2026
c80f659
Clear the framework-assigned XML resource id (view.id = View.NO_ID) i…
akfreas May 25, 2026
d26c79d
satisfy strict ESLint in OptimizationProvider injected-sdk RN tests: …
akfreas May 25, 2026
e91f741
satisfy strict ESLint in OptimizationProvider RN test: use Promise.wi…
akfreas May 25, 2026
9b8f3f5
satisfy strict ESLint in OptimizedEntry RN test: extract 1234 ms dwel…
akfreas May 25, 2026
6617c02
satisfy strict ESLint in OptimizationProvider onStatesReady react-web…
akfreas May 25, 2026
0b33758
satisfy strict ESLint in OptimizationProvider trackEntryInteraction r…
akfreas May 25, 2026
41fb92a
Tighten the two isContentfulOptimization predicates in the RN provide…
akfreas May 25, 2026
5461c03
Add the missing testID='preview-panel-scroll' to the PreviewPanel Scr…
akfreas May 25, 2026
d41f559
Apply prettier formatting to the Reflect.get type assertion on the cl…
akfreas May 25, 2026
6d48b75
Replace reset-all Alert.alert with inline confirmation view in RN pre…
akfreas May 17, 2026
0f0750a
Surface a preview-refresh-button in the RN preview panel and wire it …
akfreas May 19, 2026
02739e8
Drop set -o pipefail from the emulator-runner script — the action inv…
akfreas May 25, 2026
8d8de1e
satisfy strict ESLint in OptimizationProvider injected-sdk RN tests: …
akfreas May 25, 2026
af1dcf9
satisfy strict ESLint in OptimizationProvider RN test: use Promise.wi…
akfreas May 25, 2026
998ec0c
satisfy strict ESLint in OptimizedEntry RN test: extract 1234 ms dwel…
akfreas May 25, 2026
25407e1
satisfy strict ESLint in OptimizationProvider onStatesReady react-web…
akfreas May 25, 2026
6afc512
satisfy strict ESLint in OptimizationProvider trackEntryInteraction r…
akfreas May 25, 2026
a41b0fd
Tighten the two isContentfulOptimization predicates in the RN provide…
akfreas May 25, 2026
c5ada6a
Add the missing testID='preview-panel-scroll' to the PreviewPanel Scr…
akfreas May 25, 2026
c502e57
Apply prettier formatting to the Reflect.get type assertion on the cl…
akfreas May 25, 2026
d119016
Replace reset-all Alert.alert with inline confirmation view in RN pre…
akfreas May 17, 2026
54e44b4
Surface a preview-refresh-button in the RN preview panel and wire it …
akfreas May 19, 2026
d77c1f3
Scroll the audience-toggle row back into view between the deactivate …
akfreas May 25, 2026
bded3b6
Make scrollPanelToElement tolerate the StaleObjectException that the …
akfreas May 25, 2026
b7938ac
Sort preview-panel audiences by name only (drop the qualified-first t…
akfreas May 26, 2026
07d191c
Remove the scenario 3 scroll-between-taps workaround in PreviewPanelO…
akfreas May 26, 2026
433b355
Harden the Android UI Automator test infra against the UiAutomator-pl…
akfreas May 26, 2026
994e304
Sort preview-panel audiences by name only (drop the qualified-first t…
akfreas May 26, 2026
243975e
Remove the scenario 3 scroll-between-taps workaround in PreviewPanelO…
akfreas May 26, 2026
2744535
Harden the Android UI Automator test infra against the UiAutomator-pl…
akfreas May 26, 2026
f624d9a
Remove the stale Zipline references from the Android SDK docs and Pro…
akfreas May 26, 2026
2466eee
Rewrite PreviewPanelOverridesTests.scrollPanelToElement to use UiObje…
akfreas May 27, 2026
e9ab042
Fail-fast on first failing Android E2E test instead of letting am ins…
akfreas May 27, 2026
0c9711f
Collapse the Android E2E fail-fast pipeline onto a single physical YA…
akfreas May 27, 2026
d85979e
Cache the Android E2E AVD across CI runs to skip the 1-3 minute cold …
akfreas May 27, 2026
b549e8e
trigger CI
akfreas May 27, 2026
4cfdca7
Revert "Cache the Android E2E AVD across CI runs to skip the 1-3 minu…
akfreas May 27, 2026
f83ac45
Merge remote-tracking branch 'origin/final-mobile-sdk-fixes' into and…
akfreas May 27, 2026
c174bd7
Render the Views-path preview panel as an in-activity Compose overlay…
akfreas May 27, 2026
9c142b8
Have implementations/android-sdk/scripts/run-e2e.sh fan out to both r…
akfreas May 27, 2026
c93752a
Merge branch 'main' of github.com:contentful/optimization into androi…
akfreas May 27, 2026
0f139e1
Stop tapElement from double-tapping morphing buttons by skipping the …
akfreas May 27, 2026
ca6b010
Make PreviewPanelOverridesTests scenario 6 reliable by giving the res…
akfreas May 27, 2026
510c54f
Revert changes caused by bad merge in final-mobile-sdk-fixes
akfreas May 27, 2026
b36c538
Cap every Android UI test at 60 seconds via a shared PerTestRule that…
akfreas May 27, 2026
e897f41
Cap the Android E2E am instrument invocation at 20 minutes with timeo…
akfreas May 27, 2026
371a9b3
Add -r (raw) mode to the Android E2E am instrument invocation so Andr…
akfreas May 27, 2026
1d302d1
Fix the 43 ESLint errors in packages/universal/optimization-js-bridge…
akfreas May 27, 2026
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
176 changes: 143 additions & 33 deletions .github/workflows/main-pipeline.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -670,10 +670,10 @@ jobs:
if-no-files-found: error
retention-days: 1

e2e-android-sdk:
name: 🤖 E2E Android Native
e2e-android-sdk-build:
name: 🤖 Build Android APKs
runs-on: namespace-profile-linux-16-vcpu-32-gb-ram-optimal
timeout-minutes: 45
timeout-minutes: 30
needs: [setup, changes]
if: needs.changes.outputs.e2e_android == 'true'
env:
Expand All @@ -697,15 +697,96 @@ jobs:
echo "ANDROID_HOME=$HOME/.android/sdk" >> "$GITHUB_ENV"

- name: Prepare cache directories
run: |
mkdir -p "$HOME/.android/sdk" "$HOME/.android/avd" "$HOME/.android/cache"
run: mkdir -p "$HOME/.android/sdk" "$HOME/.android/cache"

- name: Set up caches (Namespace)
uses: namespacelabs/nscloud-cache-action@15799a6b54e5765f85b2aac25b3f0df43ed571c0 # v1.4.3
with:
cache: |
pnpm
gradle
path: |
~/.android/sdk
~/.android/cache

- name: Setup Java
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
distribution: 'temurin'
java-version: '17'

- name: Setup Android SDK
uses: android-actions/setup-android@40fd30fb8d7440372e1316f5d1809ec01dcd3699 # v4.0.1

- name: Install JS dependencies
run: pnpm install --prefer-offline --frozen-lockfile

- name: Build the JS bridge bundles
run: pnpm --filter @contentful/optimization-js-bridge build

- name: Build Compose, Views, and UI test APKs
working-directory: implementations/android-sdk
run: ./gradlew :compose:assembleDebug :views:assembleDebug :uitests:assembleDebug

- name: Collect APK artifacts at stable paths
run: |
# Use a workspace-relative directory because actions/upload-artifact@v7 keys the
# in-artifact paths off the least-common-ancestor of the inputs — an absolute
# /tmp path produces /tmp/... inside the zip, which then unpacks under the download
# target instead of into it.
mkdir -p android-apks
cp implementations/android-sdk/compose/build/outputs/apk/debug/compose-debug.apk android-apks/
cp implementations/android-sdk/views/build/outputs/apk/debug/views-debug.apk android-apks/
cp implementations/android-sdk/uitests/build/outputs/apk/debug/uitests-debug.apk android-apks/
ls -la android-apks/

- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: android-apks
path: android-apks/
if-no-files-found: error
retention-days: 1

e2e-android-sdk:
name: 🤖 E2E Android (${{ matrix.app }})
runs-on: namespace-profile-linux-16-vcpu-32-gb-ram-optimal
timeout-minutes: 45
needs: [setup, changes, e2e-android-sdk-build]
if: needs.changes.outputs.e2e_android == 'true'
strategy:
fail-fast: false
matrix:
include:
- app: compose
package: com.contentful.optimization.app
- app: views
package: com.contentful.optimization.app.views
env:
CI: 'true'
APP_PACKAGE: ${{ matrix.package }}
APP_APK: ${{ matrix.app }}-debug.apk
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: Set Android SDK environment variables
run: |
echo "ANDROID_SDK_ROOT=$HOME/.android/sdk" >> "$GITHUB_ENV"
echo "ANDROID_HOME=$HOME/.android/sdk" >> "$GITHUB_ENV"

- name: Prepare cache directories
run: mkdir -p "$HOME/.android/sdk" "$HOME/.android/avd" "$HOME/.android/cache"

- name: Set up caches (Namespace)
uses: namespacelabs/nscloud-cache-action@15799a6b54e5765f85b2aac25b3f0df43ed571c0 # v1.4.3
with:
cache: pnpm
path: |
~/.android/sdk
~/.android/avd
Expand Down Expand Up @@ -742,12 +823,10 @@ jobs:
- name: Install JS dependencies
run: pnpm install --prefer-offline --frozen-lockfile

- name: Build the JS bridge bundles
run: pnpm --filter @contentful/optimization-js-bridge build

- name: Build app and test APKs
working-directory: implementations/android-sdk
run: ./gradlew :app:assembleDebug :uitests:assembleDebug
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: android-apks
path: android-apks

- name: Start Mock Server
run: |
Expand Down Expand Up @@ -787,28 +866,53 @@ jobs:
# `set -o pipefail` would error out with "Illegal option -o pipefail" and terminate
# the script before any test ran. The grep-on-test-output checks below already
# detect instrumentation failures regardless of pipe-status propagation.
echo "Disabling animations..."
adb shell settings put global window_animation_scale 0
adb shell settings put global transition_animation_scale 0
adb shell settings put global animator_duration_scale 0
echo "Installing APKs..."
adb install -r implementations/android-sdk/app/build/outputs/apk/debug/app-debug.apk
adb install -r implementations/android-sdk/uitests/build/outputs/apk/debug/uitests-debug.apk
# The action's `disable-animations: true` already disables animation scales — no
# need to run the manual adb shell settings put lines here.
echo "Installing APKs (target app: $APP_PACKAGE)..."
adb install -r "android-apks/$APP_APK"
adb install -r android-apks/uitests-debug.apk
echo "Setting up adb reverse port forwarding..."
adb reverse tcp:8000 tcp:8000
sleep 3
adb shell "for i in 1 2 3 4 5 6 7 8 9 10; do nc -z localhost 8000 2>/dev/null && echo 'Mock server tunnel verified' && exit 0; sleep 1; done; echo 'WARNING: tunnel verification timed out'"
echo "Running UI Automator 2 E2E tests..."
echo "Resetting /sdcard/test-failures (per-test failure context dump target)..."
adb shell "rm -rf /sdcard/test-failures && mkdir -p /sdcard/test-failures"
echo "Clearing logcat ring buffer (will be dumped post-run by the workflow)..."
adb logcat -c
echo "Running UI Automator 2 E2E tests against $APP_PACKAGE..."
# Fail-fast: stream am instrument output through awk; on the first
# "Error in test" or "Process crashed" line, force-stop the test
# process to abort the remaining suite. AndroidJUnitRunner has no
# built-in early-exit, so without this every subsequent @Before
# waits its full 20-30s waitForElement timeout and one root-cause
# failure in an early class burns the whole 45-minute job budget.
# force-stop on the .uitests process kills the instrumentation;
# the remote `am instrument -w` exits and the local pipeline
# collapses. fflush keeps the GitHub Actions log live so we see
# the failure when it happens, not at the end.
# per-test error (INSTRUMENTATION_STATUS_CODE -1/-2) or device-side
# process crash, force-stop the test process to abort the remaining
# suite. AndroidJUnitRunner has no built-in early-exit, so without
# this every subsequent @Before waits its full 20-30s
# waitForElement timeout and one root-cause failure in an early
# class burns the whole 45-minute job budget. force-stop on the
# .uitests process kills the instrumentation; the remote
# `am instrument -w` exits and the local pipeline collapses.
# fflush keeps the GitHub Actions log live so we see the failure
# when it happens, not at the end.
#
# `-r` (raw mode) is REQUIRED for streaming: without it
# `am instrument` buffers the entire human-readable summary
# (dots, `OK (N tests)` / `FAILURES!!!`) until end-of-suite, so
# the awk fail-fast never sees per-test progress and a single
# 20-min hang produces zero output until the outer `timeout 1200`
# kills the pipeline (observed on run 26534799046 views matrix
# entry). With `-r`, AndroidJUnitRunner emits
# `INSTRUMENTATION_STATUS_CODE: <N>` after each test (1=started,
# 0=passed, -1=assumption-failed, -2=errored/failed) and the awk
# regex matches the failure codes in real time.
#
# Per-test wedges are caught by the JUnit `Timeout` rule in
# PerTestRule.kt (60s/test), which surfaces as a `stack=...`
# block followed by `INSTRUMENTATION_STATUS_CODE: -2`. The outer
# `timeout 1200` here is a belt-and-suspenders safety net: it
# caps total wall time at 20 min (normal happy path ~7 min) so a
# deadlock that prevents JUnit's worker-thread from firing — or
# a wedged `adb` — can't burn the full 45-min job budget. On
# `timeout` kill, the post-grep for `INSTRUMENTATION_CODE: ` (the
# final overall-run code emitted only when am instrument
# completes cleanly) is missing and the step fails.
#
# IMPORTANT: this MUST be a single physical YAML line. The
# reactivecircus/android-emulator-runner action's parseScript()
Expand All @@ -819,16 +923,20 @@ jobs:
# are dropped as no-op syntax errors. Verified in the action's
# src/script-parser.ts:
# rawScript.trim().split(/\r\n|\n|\r/)
adb shell am instrument -w com.contentful.optimization.uitests/androidx.test.runner.AndroidJUnitRunner 2>&1 | tee /tmp/test-output.log | awk 'BEGIN{aborted=0} /Error in test|Process crashed/{if(!aborted){aborted=1;print;print "::error::Test failure detected — aborting remaining suite";fflush();system("adb shell am force-stop com.contentful.optimization.uitests >/dev/null 2>&1");exit 1} next} {print;fflush()}'
grep -q "FAILURES\|Error in test" /tmp/test-output.log && { echo "::error::Android UI tests failed"; exit 1; } || true
timeout --kill-after=30 1200 adb shell am instrument -w -r -e APP_PACKAGE "$APP_PACKAGE" com.contentful.optimization.uitests/androidx.test.runner.AndroidJUnitRunner 2>&1 | tee /tmp/test-output.log | awk 'BEGIN{aborted=0} /^INSTRUMENTATION_STATUS_CODE:[[:space:]]+-[12]$|Process crashed/{if(!aborted){aborted=1;print;print "::error::Test failure detected — aborting remaining suite";fflush();system("adb shell am force-stop com.contentful.optimization.uitests >/dev/null 2>&1");exit 1} next} {print;fflush()}'; e=$?; echo "--- capturing post-run debug context (logcat + /sdcard/test-failures) ---"; adb logcat -d > /tmp/logcat.log 2>&1 || true; rm -rf /tmp/test-failures; adb pull /sdcard/test-failures /tmp/test-failures 2>&1 || true; exit $e
grep -qE "^INSTRUMENTATION_STATUS_CODE:[[:space:]]+-[12]$" /tmp/test-output.log && { echo "::error::Android UI tests failed (per-test failure detected)"; exit 1; } || true
grep -q "Process crashed" /tmp/test-output.log && { echo "::error::Test process crashed"; exit 1; } || true
grep -q "OK (" /tmp/test-output.log || { echo "::error::Android UI tests did not complete successfully (missing OK status)"; exit 1; }
grep -qE "^INSTRUMENTATION_CODE:[[:space:]]+-?[0-9]+$" /tmp/test-output.log || { echo "::error::Android UI tests did not complete cleanly (missing terminal INSTRUMENTATION_CODE — likely timeout, hang, or am instrument abort)"; exit 1; }

- name: Upload logs on failure
- name: Print debug context on failure
if: failure()
run: |
echo "=== Mock Server Logs ==="
cat /tmp/mock-server.log || echo "No mock server logs found"
echo "=== Per-test failure captures (/tmp/test-failures) ==="
ls -la /tmp/test-failures 2>/dev/null || echo "No per-test failure captures pulled"
echo "=== Logcat tail (/tmp/logcat.log, last 500 lines) ==="
tail -n 500 /tmp/logcat.log 2>/dev/null || echo "No logcat dump found"

- name: Stop Mock Server
if: always()
Expand All @@ -838,11 +946,13 @@ jobs:
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: always()
with:
name: ci-results-android-sdk
name: ci-results-android-sdk-${{ matrix.app }}
path: |
implementations/android-sdk/logs/
/tmp/mock-server.log
/tmp/test-output.log
/tmp/logcat.log
/tmp/test-failures/
retention-days: 7

e2e-ios-sdk-build:
Expand Down
3 changes: 0 additions & 3 deletions eslint.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,6 @@ export default defineConfig(
'**/dist',
'docs/media/**',
'**/ios/**',
// Engine-targeted JS bridge glue compiled into the native SDKs; consolidated
// from the ios/android bridge packages, which were ignored under the rules above.
'**/optimization-js-bridge/**',
'**/node_modules',
],
},
Expand Down
Loading
Loading