Skip to content

Commit a6728bf

Browse files
Fixing BCIT and in app tests in CI (#1048)
Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent d457147 commit a6728bf

7 files changed

Lines changed: 229 additions & 146 deletions

File tree

.github/scripts/run-e2e.sh

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
#!/usr/bin/env bash
2+
# .github/scripts/run-e2e.sh
3+
#
4+
# Runs the In-App Message E2E test under ReactiveCircus/android-emulator-runner
5+
# and captures diagnostics that survive the action's emulator-kill on exit.
6+
#
7+
# Why this is an external script and not inline YAML:
8+
# The action runs each line of `script:` in a fresh `/bin/sh -c`, so cross-line
9+
# variables and shell functions don't survive. We need a single bash process for
10+
# the trap + variable + function semantics.
11+
#
12+
# Inputs (env, all set by the workflow step):
13+
# ITERABLE_API_KEY — set as buildConfigField at runtime; not echoed.
14+
# ITERABLE_SERVER_API_KEY — set as buildConfigField at runtime; not echoed.
15+
# ITERABLE_TEST_USER_EMAIL — used by tests; not echoed (length only).
16+
# GITHUB_WORKSPACE — set by the runner; root for diagnostics output.
17+
#
18+
# Outputs:
19+
# $GITHUB_WORKSPACE/integration-tests/build/diagnostics/
20+
# hierarchy.xml — UiAutomator dump at the moment of test exit
21+
# screenshot.png — device screenshot at the moment of test exit
22+
# logcat.txt — full device logcat from start of test invocation
23+
#
24+
# Exit code:
25+
# The gradle test task's exit code, propagated.
26+
#
27+
# This script writes nothing outside $GITHUB_WORKSPACE/integration-tests/build/.
28+
29+
set -uo pipefail
30+
31+
readonly TEST_CLASS="${TEST_CLASS:-com.iterable.integration.tests.InAppMessageIntegrationTest#testInAppMessageMVP}"
32+
readonly DIAG_DIR="${GITHUB_WORKSPACE:?GITHUB_WORKSPACE must be set}/integration-tests/build/diagnostics"
33+
readonly TEST_PACKAGE="com.iterable.integration.tests"
34+
35+
mkdir -p "$DIAG_DIR"
36+
37+
log() { printf '\033[1;34m[e2e]\033[0m %s\n' "$*"; }
38+
39+
log "Running E2E test: $TEST_CLASS"
40+
log "Diagnostics will be written to: $DIAG_DIR"
41+
42+
# Sanity-check env: don't echo secret values, only their lengths. The workflow's
43+
# env: block guarantees these vars exist; ${#VAR} of an empty string is 0.
44+
log "ITERABLE_API_KEY length: ${#ITERABLE_API_KEY}"
45+
log "ITERABLE_SERVER_API_KEY length: ${#ITERABLE_SERVER_API_KEY}"
46+
log "ITERABLE_TEST_USER_EMAIL length: ${#ITERABLE_TEST_USER_EMAIL}"
47+
48+
# Grant permissions; ignore failures (the package may not be installed yet,
49+
# in which case AGP will install + auto-grant during the test step).
50+
for perm in POST_NOTIFICATIONS INTERNET ACCESS_NETWORK_STATE WAKE_LOCK; do
51+
adb shell pm grant "$TEST_PACKAGE" "android.permission.$perm" >/dev/null 2>&1 || true
52+
done
53+
54+
# Stream full logcat to the workspace so the artifact upload always has it.
55+
adb logcat -c >/dev/null 2>&1 || true
56+
adb logcat > "$DIAG_DIR/logcat.txt" &
57+
LOGCAT_PID=$!
58+
59+
# Capture diagnostics that depend on a live emulator. Called from EXIT trap so
60+
# we always run, whether tests passed, failed, or the runner timed out.
61+
#
62+
# SDK-170: every adb call here is wrapped with `timeout` so an unresponsive
63+
# emulator (e.g. on test failure) can't make the diagnostic capture itself
64+
# hang the 6-hour job timeout — which would replace the useful gradle
65+
# failure output with an opaque cancelled-job. 10s per command is generous
66+
# (uiautomator dump usually finishes in <2s on a healthy device).
67+
ADB_TIMEOUT="${ADB_TIMEOUT:-10}"
68+
69+
capture_post_test() {
70+
log "Capturing post-test diagnostics..."
71+
72+
# Stop logcat first so the file isn't being appended to mid-copy.
73+
if [[ -n "${LOGCAT_PID:-}" ]]; then
74+
kill "$LOGCAT_PID" 2>/dev/null || true
75+
wait "$LOGCAT_PID" 2>/dev/null || true
76+
fi
77+
78+
# UiAutomator hierarchy — answers "what was UiAutomator looking at?"
79+
if timeout "$ADB_TIMEOUT" adb shell uiautomator dump /sdcard/hierarchy.xml >/dev/null 2>&1; then
80+
timeout "$ADB_TIMEOUT" adb pull /sdcard/hierarchy.xml "$DIAG_DIR/hierarchy.xml" >/dev/null 2>&1 || true
81+
timeout "$ADB_TIMEOUT" adb shell rm -f /sdcard/hierarchy.xml >/dev/null 2>&1 || true
82+
else
83+
log "uiautomator dump unavailable (emulator unresponsive or no device)"
84+
fi
85+
86+
# Screenshot — answers "what was actually on the screen?"
87+
if timeout "$ADB_TIMEOUT" adb shell screencap -p /sdcard/screenshot.png >/dev/null 2>&1; then
88+
timeout "$ADB_TIMEOUT" adb pull /sdcard/screenshot.png "$DIAG_DIR/screenshot.png" >/dev/null 2>&1 || true
89+
timeout "$ADB_TIMEOUT" adb shell rm -f /sdcard/screenshot.png >/dev/null 2>&1 || true
90+
else
91+
log "screencap unavailable (emulator unresponsive or no device)"
92+
fi
93+
94+
log "Diagnostics captured:"
95+
ls -la "$DIAG_DIR" || true
96+
}
97+
trap capture_post_test EXIT
98+
99+
# Run the test. Don't `set -e`; we want to capture diagnostics on failure and
100+
# propagate the original exit code at the end.
101+
gradle_exit=0
102+
./gradlew :integration-tests:connectedDebugAndroidTest \
103+
-Pandroid.testInstrumentationRunnerArguments.class="$TEST_CLASS" \
104+
--stacktrace --no-daemon || gradle_exit=$?
105+
106+
if [[ "$gradle_exit" -ne 0 ]]; then
107+
log "::error::Gradle test task failed with exit code $gradle_exit — see e2e-diagnostics-api artifact"
108+
fi
109+
110+
# capture_post_test runs via EXIT trap; just propagate the exit code.
111+
exit "$gradle_exit"

.github/workflows/build.yml

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,13 @@ jobs:
1313
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
1414

1515
- name: Validate Gradle Wrapper
16-
uses: gradle/wrapper-validation-action@216d1ad2b3710bf005dc39237337b9673fd8fcd5 # v3.3.2
16+
uses: gradle/actions/wrapper-validation@af1da67850ed9a4cedd57bfd976089dd991e2582 # v4.0.0
1717

1818
- name: Configure JDK
19-
uses: actions/setup-java@d202f5dbf7256730fb690ec59f6381650114feb2 # v1.4.3
19+
uses: actions/setup-java@c1e323688fd81a25caa38c78aa6df2d33d3e20d9 # v4.8.0
2020
with:
21-
java-version: 17
21+
distribution: temurin
22+
java-version: '17'
2223

2324
- run: touch local.properties
2425

@@ -36,12 +37,13 @@ jobs:
3637
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
3738

3839
- name: Validate Gradle Wrapper
39-
uses: gradle/wrapper-validation-action@216d1ad2b3710bf005dc39237337b9673fd8fcd5 # v3.3.2
40+
uses: gradle/actions/wrapper-validation@af1da67850ed9a4cedd57bfd976089dd991e2582 # v4.0.0
4041

4142
- name: Configure JDK
42-
uses: actions/setup-java@d202f5dbf7256730fb690ec59f6381650114feb2 # v1.4.3
43+
uses: actions/setup-java@c1e323688fd81a25caa38c78aa6df2d33d3e20d9 # v4.8.0
4344
with:
44-
java-version: 17
45+
distribution: temurin
46+
java-version: '17'
4547

4648
- run: touch local.properties
4749

@@ -66,12 +68,13 @@ jobs:
6668
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
6769

6870
- name: Validate Gradle Wrapper
69-
uses: gradle/wrapper-validation-action@216d1ad2b3710bf005dc39237337b9673fd8fcd5 # v3.3.2
71+
uses: gradle/actions/wrapper-validation@af1da67850ed9a4cedd57bfd976089dd991e2582 # v4.0.0
7072

7173
- name: Configure JDK
72-
uses: actions/setup-java@d202f5dbf7256730fb690ec59f6381650114feb2 # v1.4.3
74+
uses: actions/setup-java@c1e323688fd81a25caa38c78aa6df2d33d3e20d9 # v4.8.0
7375
with:
74-
java-version: 17
76+
distribution: temurin
77+
java-version: '17'
7578

7679
- run: touch local.properties
7780

.github/workflows/inapp-e2e-tests.yml

Lines changed: 38 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,27 @@ on:
1010
jobs:
1111
inapp-e2e-tests:
1212
name: In-App Message E2E Tests
13-
runs-on: macos-15-intel
14-
13+
# SDK-170: macOS Intel runners (2 cores / 3GB AVD on HVF) starved system_server during
14+
# cold boot and produced cascading ANRs (systemui / nexuslauncher / gms / phone …),
15+
# leaving a system dialog on top of MainActivity so UiAutomator could not find the
16+
# in-app button. Ubuntu runners with KVM acceleration and 4 vCPU / 16GB stop the storm.
17+
runs-on: ubuntu-latest
18+
1519
strategy:
1620
matrix:
1721
api-level: [34] # MVP testing on most relevant API level only
18-
22+
1923
steps:
2024
- name: Checkout code
2125
uses: actions/checkout@v4
22-
26+
27+
- name: Enable KVM device permissions
28+
run: |
29+
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' \
30+
| sudo tee /etc/udev/rules.d/99-kvm.rules
31+
sudo udevadm control --reload-rules
32+
sudo udevadm trigger --name-match=kvm
33+
2334
- name: Set up JDK 17
2435
uses: actions/setup-java@v4
2536
with:
@@ -69,15 +80,15 @@ jobs:
6980
./gradlew :integration-tests:assembleDebug :integration-tests:assembleDebugAndroidTest --no-daemon &
7081
echo "Build started in background..."
7182
72-
- name: Run UI Tests with Emulator (Intel / x86_64)
83+
- name: Run UI Tests with Emulator (KVM / x86_64)
7384
uses: ReactiveCircus/android-emulator-runner@v2
7485
with:
7586
api-level: ${{ matrix.api-level }}
7687
target: google_apis
7788
arch: x86_64
7889
profile: pixel_6
79-
cores: 2
80-
ram-size: 3072M
90+
cores: 4
91+
ram-size: 4096M
8192
heap-size: 576M
8293
force-avd-creation: true
8394
disable-animations: true
@@ -87,98 +98,30 @@ jobs:
8798
# Clean + start adb after platform-tools exist (avoids tcp:5037 noise)
8899
adb kill-server >/dev/null 2>&1 || true
89100
adb start-server
90-
script: |
91-
echo "Emulator is ready! Running tests..."
92-
echo "Setting up permissions..."
93-
adb shell pm grant com.iterable.integration.tests android.permission.POST_NOTIFICATIONS
94-
adb shell pm grant com.iterable.integration.tests android.permission.INTERNET
95-
adb shell pm grant com.iterable.integration.tests android.permission.ACCESS_NETWORK_STATE
96-
adb shell pm grant com.iterable.integration.tests android.permission.WAKE_LOCK
97-
98-
echo "Running In-App Message MVP test..."
99-
echo "Debug: Checking if APKs are ready..."
100-
ls -la integration-tests/build/outputs/apk/ || echo "APK directory not found"
101-
102-
echo "Debug: Verifying API keys are set..."
103-
echo "ITERABLE_API_KEY length: ${#ITERABLE_API_KEY}"
104-
echo "ITERABLE_SERVER_API_KEY length: ${#ITERABLE_SERVER_API_KEY}"
105-
echo "ITERABLE_TEST_USER_EMAIL: $ITERABLE_TEST_USER_EMAIL"
106-
107-
# Start logcat in background for crash debugging
108-
adb logcat > /tmp/test-logcat.log &
109-
LOGCAT_PID=$!
110-
111-
# Run the specific test with better error handling
112-
./gradlew :integration-tests:connectedDebugAndroidTest \
113-
-Pandroid.testInstrumentationRunnerArguments.class=com.iterable.integration.tests.InAppMessageIntegrationTest#testInAppMessageMVP \
114-
--stacktrace --no-daemon || {
115-
echo "Test failed! Collecting crash logs..."
116-
kill $LOGCAT_PID 2>/dev/null || true
117-
echo "=== CRASH LOGS ==="
118-
tail -100 /tmp/test-logcat.log
119-
echo "=== END CRASH LOGS ==="
120-
exit 1
121-
}
122-
123-
# Stop logcat
124-
kill $LOGCAT_PID 2>/dev/null || true
101+
# The android-emulator-runner action runs each line of an inline `script:`
102+
# in a fresh `/bin/sh -c`, so cross-line variables and bash functions don't
103+
# survive. Externalise the whole thing to a single bash file that runs in
104+
# one process — see .github/scripts/run-e2e.sh for the actual logic.
105+
script: bash "$GITHUB_WORKSPACE/.github/scripts/run-e2e.sh"
125106
env:
126107
ITERABLE_API_KEY: ${{ secrets.BCIT_ITERABLE_API_KEY }}
127108
ITERABLE_SERVER_API_KEY: ${{ secrets.BCIT_ITERABLE_SERVER_API_KEY }}
128109
ITERABLE_TEST_USER_EMAIL: ${{ secrets.BCIT_ITERABLE_TEST_USER_EMAIL }}
129-
130-
# - name: Generate Test Report
131-
# if: always()
132-
# run: |
133-
# echo "Generating E2E test report..."
134-
# ./gradlew :integration-tests:jacocoIntegrationTestReport
135-
136-
# - name: Collect Test Logs
137-
# if: always()
138-
# run: |
139-
# echo "Collecting E2E test logs..."
140-
# adb logcat -d > integration-tests/build/e2e-test-logs.txt
141-
142-
# # Also collect specific test logs
143-
# adb logcat -d | grep -E "(InAppMessageIntegrationTest|BaseIntegrationTest|IterableApi)" > integration-tests/build/inapp-specific-logs.txt
144-
145-
# - name: Take Screenshots for Debugging
146-
# if: always()
147-
# run: |
148-
# echo "Taking screenshots for debugging..."
149-
# mkdir -p integration-tests/screenshots
150-
# adb shell screencap -p /sdcard/screenshot.png
151-
# adb pull /sdcard/screenshot.png integration-tests/screenshots/final-state-api-${{ matrix.api-level }}.png
152-
153-
# - name: Upload Test Results
154-
# if: always()
155-
# uses: actions/upload-artifact@v4
156-
# with:
157-
# name: inapp-e2e-test-results-api-${{ matrix.api-level }}
158-
# path: |
159-
# integration-tests/build/reports/
160-
# integration-tests/build/outputs/
161-
# integration-tests/build/e2e-test-logs.txt
162-
# integration-tests/build/inapp-specific-logs.txt
163-
164-
# - name: Upload Coverage Report
165-
# if: always()
166-
# uses: actions/upload-artifact@v4
167-
# with:
168-
# name: inapp-e2e-coverage-api-${{ matrix.api-level }}
169-
# path: integration-tests/build/reports/jacoco/
170-
171-
# - name: Upload Screenshots
172-
# if: always()
173-
# uses: actions/upload-artifact@v4
174-
# with:
175-
# name: inapp-e2e-screenshots-api-${{ matrix.api-level }}
176-
# path: integration-tests/screenshots/
177-
178-
# - name: Cleanup
179-
# if: always()
180-
# run: |
181-
# echo "Test cleanup completed"
110+
111+
# SDK-170: do NOT upload integration-tests/build/outputs/ — that path contains the
112+
# built APKs which embed BuildConfig.ITERABLE_API_KEY and BuildConfig.ITERABLE_SERVER_API_KEY
113+
# as compile-time string constants. On a public repo, anyone who can download the
114+
# artifact could `strings`/`apktool` the APK and recover both keys.
115+
- name: Upload E2E diagnostics
116+
if: always()
117+
uses: actions/upload-artifact@v4
118+
with:
119+
name: e2e-diagnostics-api-${{ matrix.api-level }}
120+
path: |
121+
integration-tests/build/diagnostics/
122+
integration-tests/build/reports/
123+
if-no-files-found: warn
124+
retention-days: 7
182125

183126
# test-summary:
184127
# name: Test Summary

0 commit comments

Comments
 (0)