Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 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
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
243 changes: 243 additions & 0 deletions .github/workflows/main-pipeline.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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_android: ${{ steps.filter.outputs.e2e_android }}
e2e_ios: ${{ steps.filter.outputs.e2e_ios }}
steps:
- uses: namespacelabs/nscloud-checkout-action@938f5d2d403d6224d9a0c0dc559b1dae09c2ede4 # v8.1.1
Expand Down Expand Up @@ -126,11 +127,21 @@ jobs:
- 'package.json'
- 'pnpm-lock.yaml'
- '.github/workflows/main-pipeline.yaml'
# Android native implementation E2E coverage scope.
e2e_android:
- 'implementations/android-sdk/**'
- 'lib/mocks/**'
- 'packages/android/**'
- 'packages/universal/optimization-js-bridge/**'
- '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/**'
- 'packages/universal/optimization-js-bridge/**'
- 'package.json'
- 'pnpm-lock.yaml'
- '.github/workflows/main-pipeline.yaml'
Expand Down Expand Up @@ -659,6 +670,238 @@ jobs:
if-no-files-found: error
retention-days: 1

e2e-android-sdk-build:
name: 🤖 Build Android APKs
runs-on: namespace-profile-linux-16-vcpu-32-gb-ram-optimal
timeout-minutes: 30
needs: [setup, changes]
if: needs.changes.outputs.e2e_android == 'true'
env:
CI: 'true'
GRADLE_OPTS: >-
-Dorg.gradle.daemon=false -Dorg.gradle.parallel=true -Dorg.gradle.jvmargs=-Xmx4g
-Dkotlin.daemon.jvm.options=-Xmx2g
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/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
~/.android/cache

- name: Install system dependencies (Android emulator)
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
ca-certificates curl unzip zip git \
netcat-openbsd cpu-checker \
libgl1 libnss3 libx11-6 libx11-xcb1 libxcomposite1 libxdamage1 libxrandr2 libxtst6 \
libxi6 libxrender1 libxkbcommon0 libgbm1 libdbus-1-3 libdrm2 libpulse0
sudo apt-get install -y --no-install-recommends libasound2 || sudo apt-get install -y --no-install-recommends libasound2t64

- name: Verify KVM is available
run: |
if [ ! -e /dev/kvm ]; then
echo "/dev/kvm not found; Android hardware acceleration will not work." >&2
exit 1
fi
ls -l /dev/kvm
sudo kvm-ok || true

- 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

- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: android-apks
path: android-apks

- 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 Android E2E Tests (emulator)
uses: reactivecircus/android-emulator-runner@e89f39f1abbbd05b1113a29cf4db69e7540cae5a # v2.37.0
with:
api-level: 35
arch: x86_64
target: google_apis
profile: pixel_7
avd-name: test
force-avd-creation: true
emulator-boot-timeout: 600
cores: 6
ram-size: 4096M
disk-size: 8G
disable-animations: true
emulator-options: -no-window -no-audio -no-boot-anim -gpu swiftshader_indirect
script: |
# The emulator-runner action invokes the script via `/usr/bin/sh -c`, not bash, so
# `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.
# 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 against $APP_PACKAGE..."
adb shell am instrument -w -e APP_PACKAGE "$APP_PACKAGE" com.contentful.optimization.uitests/androidx.test.runner.AndroidJUnitRunner 2>&1 | tee /tmp/test-output.log
grep -q "FAILURES" /tmp/test-output.log && { echo "::error::Android UI tests failed"; 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; }

- name: Upload logs on failure
if: failure()
run: |
echo "=== Mock Server Logs ==="
cat /tmp/mock-server.log || echo "No mock server logs found"

- 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: always()
with:
name: ci-results-android-sdk-${{ matrix.app }}
path: |
implementations/android-sdk/logs/
/tmp/mock-server.log
/tmp/test-output.log
retention-days: 7

e2e-ios-sdk-build:
name: 🍎 Build iOS UI Test Bundles
runs-on: namespace-profile-macos-apple-silicon-arm64-6-cpu-14-gb
Expand Down
42 changes: 36 additions & 6 deletions .github/workflows/notify-slack.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ jobs:
)"

has_docs=false
has_guides=false
has_concepts=false
has_packages=false
changed_packages=""

Expand All @@ -49,12 +51,20 @@ jobs:
case "$file" in
documentation/drafts/*)
;;
documentation/guides/*)
has_docs=true
has_guides=true
;;
documentation/concepts/*)
has_docs=true
has_concepts=true
;;
documentation/*)
has_docs=true
;;
packages/ios/ios-jsc-bridge/*)
packages/universal/optimization-js-bridge/*)
has_packages=true
add_package "@contentful/optimization-ios-bridge"
add_package "@contentful/optimization-js-bridge"
;;
packages/node/node-sdk/*)
has_packages=true
Expand Down Expand Up @@ -103,14 +113,34 @@ jobs:
)"

if [ "$has_docs" = "true" ] && [ "$has_packages" = "true" ]; then
message_title="📝📦 Documentation and packages just landed"
message_body="Documentation has been updated, and package changes were merged. @TECH_WRITER@, fresh docs goodness just landed for your reading list!"
if [ "$has_guides" = "true" ] && [ "$has_concepts" = "true" ]; then
message_title="📚📦 Docs and packages just landed"
message_body="Guide, concept, and package updates all landed together. @TECH_WRITER@, there is fresh guide goodness in the mix for your reading list!"
elif [ "$has_guides" = "true" ]; then
message_title="🧭📦 Guides and packages just landed"
message_body="Fresh guide updates and package changes are live. @TECH_WRITER@, something new and useful just landed in your favorite corner of the docs!"
elif [ "$has_concepts" = "true" ]; then
message_title="💡📦 Concepts and packages just landed"
message_body="Concept docs and package updates moved forward together. A tidy little upgrade for readers and builders!"
else
message_title="📝📦 Docs and packages just landed"
message_body="Documentation and package updates were merged together. Better docs, fresher packages!"
fi
elif [ "$has_packages" = "true" ]; then
message_title="📦 Package changes just landed"
message_body="Package updates were merged. Fresh bits are ready for the next build!"
elif [ "$has_guides" = "true" ] && [ "$has_concepts" = "true" ]; then
message_title="📚 Guides and concepts just landed"
message_body="Guide and concept documentation both got updates. @TECH_WRITER@, fresh guide goodness just landed for your reading list!"
elif [ "$has_guides" = "true" ]; then
message_title="🧭 Guide documentation just landed"
message_body="Fresh guide updates are live. @TECH_WRITER@, something new and useful just landed in your favorite corner of the docs!"
elif [ "$has_concepts" = "true" ]; then
message_title="💡 Concept documentation just landed"
message_body="Concept docs got a little clearer today. Nice boost for the next reader!"
else
message_title="📝 Documentation just landed"
message_body="Documentation has been updated. @TECH_WRITER@, fresh docs goodness just landed for your reading list!"
message_body="A documentation update was merged. Small polish, better docs!"
fi

{
Expand Down Expand Up @@ -141,7 +171,7 @@ jobs:

if [[ "$message_body" == *"@TECH_WRITER@"* ]]; then
if [ -z "$SLACK_TECH_WRITER_ID" ]; then
echo "SLACK_TECH_WRITER_ID is required for documentation notifications." >&2
echo "SLACK_TECH_WRITER_ID is required for guide notifications." >&2
exit 1
fi

Expand Down
15 changes: 7 additions & 8 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -70,14 +70,12 @@ local.properties
*.keystore
!debug.keystore
!**/gradle/wrapper/gradle-wrapper.jar
**/android-sdk/.gradle/
**/android-sdk/app/build/
**/android-sdk/uitests/build/
**/android-sdk/local.properties
**/android-sdk/logs/

# Android Native
triage-out
.gradle/
app/build/
uitests/build/
local.properties
logs/

# node.js
#
Expand Down Expand Up @@ -122,4 +120,5 @@ yarn-error.log
# Local environment configuration - commented out since we use safe defaults
# Uncomment this line if you need to override with local secrets
# env.config.ts

triage-out
.claude/
3 changes: 2 additions & 1 deletion .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ pnpm-lock.yaml
**/android/.gradle/**
**/.bundle/**
**/node_modules/**
packages/ios/ContentfulOptimization/Sources/ContentfulOptimization/Resources/optimization-ios-bridge.umd.js
packages/ios/ContentfulOptimization/Sources/ContentfulOptimization/Resources/optimization-ios-bridge.umd.js
packages/android/ContentfulOptimization/src/main/assets/optimization-android-bridge.umd.js
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,8 @@ React Native support is available through
[`@contentful/optimization-react-native`](./packages/react-native-sdk/README.md).

Native iOS work is also present in this repository as a pre-release Swift Package under
[`packages/ios`](./packages/ios/README.md), backed by the
[`@contentful/optimization-ios-bridge`](./packages/ios/ios-jsc-bridge/README.md) JavaScriptCore
[`packages/ios`](./packages/ios/README.md), backed by the shared
[`@contentful/optimization-js-bridge`](./packages/universal/optimization-js-bridge/README.md)
adapter and the [iOS reference app](./implementations/ios-sdk/README.md). Treat this surface as
alpha implementation work rather than a stable public native SDK.

Expand Down
Loading
Loading