API 37 was the only red job in the matrix, and the last two fixes each corrected a real problem only to reveal the next one. This is the cause of the third failure. The emulator raises an undersized guest to 2560M on its own, but only for API levels it recognises, and it does not recognise "37.0". Comparing the two CI logs from the same emulator binary (37.1.11.0) shows the asymmetry directly: the API 36 job logs "Increasing RAM size to 2560MB" and the API 37 job has no such line. So four levels were quietly running at 2560M while API 37 ran at the pixel_6 default of 1536M, lost system_server partway through installing the 82 MB APK, and surfaced it as "Can't find service: package". 2560M is not a guess at a sufficient value -- it is the value the other four levels already pass at, so this makes the matrix uniform rather than introducing a fifth configuration. Verified that the setting actually lands: the action appends hw.ramSize to a config.ini that already has one from the profile, so the fix only works if the later key wins. Appending a distinctive 3072M to an API 36 AVD produced MemTotal 3047924 kB and suppressed the automatic bump, confirming it does. This failure cannot be reproduced locally -- API 37 will not boot on a workstation under either GPU mode, aborting surfaceflinger in the goldfish mapper under -gpu host and segfaulting the emulator under swiftshader_indirect -- so the job now reports guest memory on every run and dumps OOM kills and native crashes on failure. That makes the next run conclusive either way instead of producing another bare "Can't find service: package". Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
212 lines
10 KiB
YAML
212 lines
10 KiB
YAML
name: Status check
|
|
|
|
on:
|
|
pull_request:
|
|
branches: [main]
|
|
|
|
# A newer push to the same PR makes the in-flight run obsolete. An emulator matrix is
|
|
# expensive, so cancel rather than let runs pile up.
|
|
concurrency:
|
|
group: status-check-${{ github.ref }}
|
|
cancel-in-progress: true
|
|
|
|
# Third-party actions are pinned to a commit rather than a tag. A tag is mutable: the
|
|
# owner can repoint v4 at new code, so a tag reference is an open invitation to run
|
|
# whatever that repository contains tomorrow. The trailing comment records which
|
|
# release each hash corresponds to, since a bare hash is unreadable.
|
|
env:
|
|
GRADLE_CACHE_PATHS: |
|
|
~/.gradle/caches
|
|
~/.gradle/wrapper
|
|
|
|
jobs:
|
|
# ---------------------------------------------------------------------------
|
|
# Validates the committed FFmpeg archive. It does not build anything: the whole
|
|
# point of checking the binary in is that a red run means broken code rather than
|
|
# a cross-compile that hiccuped.
|
|
#
|
|
# Its own job so a bad archive reports once, clearly, instead of surfacing as five
|
|
# confusing emulator failures. It takes seconds, so gating the matrix on it costs
|
|
# almost nothing.
|
|
# ---------------------------------------------------------------------------
|
|
ffmpeg:
|
|
name: FFmpeg binary
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 10
|
|
steps:
|
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
|
|
- name: Verify the committed archive
|
|
run: |
|
|
AAR=bin/ffmpeg-kit-next-8.1.1.aar
|
|
test -f "$AAR" || { echo "::error::$AAR is missing"; exit 1; }
|
|
|
|
# A truncated file, or a Git LFS pointer checked out without LFS, would pass
|
|
# a file-exists check and then surface much later as a confusing linker
|
|
# error. Assert the archive actually carries native libraries for both ABIs.
|
|
for abi in arm64-v8a x86_64; do
|
|
n=$(unzip -l "$AAR" | grep -c "jni/$abi/.*\.so$" || true)
|
|
echo " $abi: $n shared libraries"
|
|
test "$n" -gt 0 || { echo "::error::AAR has no $abi libraries"; exit 1; }
|
|
done
|
|
|
|
# 16 KB alignment is a Play requirement and is easy to lose in a rebuild,
|
|
# so it is checked here rather than discovered at submission.
|
|
unzip -q -o "$AAR" 'jni/*' -d /tmp/aarcheck
|
|
bad=0
|
|
for f in /tmp/aarcheck/jni/*/*.so; do
|
|
align=$(readelf -lW "$f" | awk '$1=="LOAD"{print $NF}' | sort -u)
|
|
if [ "$align" != "0x4000" ]; then
|
|
echo "::error::$(basename "$f") is $align, not 16 KB aligned"; bad=1
|
|
fi
|
|
done
|
|
test "$bad" -eq 0 || exit 1
|
|
echo " all libraries are 16 KB aligned"
|
|
|
|
# Record what shipped, so a failing run elsewhere can be tied to a version.
|
|
echo " sha256: $(sha256sum "$AAR" | cut -d' ' -f1)"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# JVM tests: the routing matrix, the FFmpeg argument builder, the concat planner
|
|
# and the retry rule. No device needed, so this is the fastest signal on a PR.
|
|
# ---------------------------------------------------------------------------
|
|
unit:
|
|
name: Unit tests
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 30
|
|
steps:
|
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
|
|
- uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
|
|
with:
|
|
distribution: temurin
|
|
java-version: '17' # AGP 9 will not run on anything older
|
|
|
|
# Gradle is invoked through the committed wrapper rather than a setup action.
|
|
# The wrapper verifies its own distribution against distributionSha256Sum, and
|
|
# caching is a handful of lines, so the action earned little here.
|
|
- uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
|
with:
|
|
path: ${{ env.GRADLE_CACHE_PATHS }}
|
|
key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle.kts', 'gradle/libs.versions.toml', 'gradle/wrapper/gradle-wrapper.properties') }}
|
|
restore-keys: gradle-${{ runner.os }}-
|
|
|
|
- name: Unit tests
|
|
run: ./gradlew :app:testDebugUnitTest
|
|
|
|
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
if: always()
|
|
with:
|
|
name: unit-test-report
|
|
path: app/build/reports/tests/
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# One runner per API level, across the whole supported range.
|
|
#
|
|
# The range is the point: minSdk is 33 and targetSdk is 37, and the foreground
|
|
# service type differs across it -- none below 34, dataSync at 34, mediaProcessing
|
|
# from 35. Testing a single level would leave two thirds of that branch unexercised.
|
|
#
|
|
# FFmpeg is not built here. The AAR is committed under bin/, so a red run means the
|
|
# code is broken rather than that a cross-compile hiccuped.
|
|
# ---------------------------------------------------------------------------
|
|
e2e:
|
|
name: E2E API ${{ matrix.label }}
|
|
runs-on: ubuntu-latest
|
|
needs: ffmpeg
|
|
timeout-minutes: 60
|
|
strategy:
|
|
# Report every API level rather than stopping at the first red one. Knowing
|
|
# whether a failure is universal or specific to one level is most of the
|
|
# diagnosis.
|
|
fail-fast: false
|
|
matrix:
|
|
include:
|
|
- label: "33"
|
|
api-level: "33"
|
|
- label: "34"
|
|
api-level: "34"
|
|
- label: "35"
|
|
api-level: "35"
|
|
- label: "36"
|
|
api-level: "36"
|
|
# API 37 ships as android-37.0. The emulator action installs
|
|
# "platforms;android-${api-level}" and there is no platforms;android-37,
|
|
# so a bare 37 fails during SDK setup before an emulator ever starts.
|
|
# system-image-api-level alone does not fix it: that only names the image.
|
|
- label: "37"
|
|
api-level: "37.0"
|
|
steps:
|
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
|
|
- uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
|
|
with:
|
|
distribution: temurin
|
|
java-version: '17'
|
|
|
|
- uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
|
with:
|
|
path: ${{ env.GRADLE_CACHE_PATHS }}
|
|
key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle.kts', 'gradle/libs.versions.toml', 'gradle/wrapper/gradle-wrapper.properties') }}
|
|
restore-keys: gradle-${{ runner.os }}-
|
|
|
|
# Without this the emulator falls back to software rendering and takes minutes
|
|
# longer to boot, when it boots at all.
|
|
- name: Enable KVM
|
|
run: |
|
|
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' \
|
|
| sudo tee /etc/udev/rules.d/99-kvm4all.rules
|
|
sudo udevadm control --reload-rules
|
|
sudo udevadm trigger --name-match=kvm
|
|
|
|
- name: Instrumented tests
|
|
uses: reactivecircus/android-emulator-runner@a421e43855164a8197daf9d8d40fe71c6996bb0d # v2.38.0
|
|
with:
|
|
api-level: ${{ matrix.api-level }}
|
|
target: google_apis
|
|
arch: x86_64
|
|
profile: pixel_6
|
|
# swiftshader_indirect is correct here only because runners have no GPU to
|
|
# pass through. On a workstation the same setting routes through
|
|
# SwiftShader's JIT, which is a known crash source.
|
|
emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
|
|
disable-animations: true
|
|
# The default userdata partition is not big enough for this APK once the
|
|
# FFmpeg libraries are in it. API 37's system image leaves the least room and
|
|
# was the level that actually failed, with "Requested internal only, but not
|
|
# enough space" -- but the margin was thin everywhere, so give them all room.
|
|
disk-size: 8G
|
|
# The emulator raises an undersized guest to 2560M by itself, but only for
|
|
# API levels it recognises, and it does not recognise "37.0". Every green job
|
|
# in this matrix was quietly running at 2560M while API 37 ran at the pixel_6
|
|
# default of 1536M, lost system_server partway through installing the 82 MB
|
|
# APK, and reported it as "Can't find service: package". 2560M is not a guess
|
|
# at a sufficient value: it is the value the other four levels already pass
|
|
# at. Setting it explicitly makes the matrix uniform instead of leaving one
|
|
# level at the mercy of that heuristic.
|
|
ram-size: 2560M
|
|
# Build only the ABI the emulator can execute. FFmpeg's native libraries
|
|
# dominate the APK, so shipping arm64 to an x86_64 emulator doubles the
|
|
# install for code that can never run: 114 MB against 80 MB.
|
|
#
|
|
# The memory probes exist because this failure cannot be reproduced locally:
|
|
# API 37 will not boot on a workstation under either GPU mode -- host aborts
|
|
# surfaceflinger in the goldfish mapper, swiftshader_indirect segfaults the
|
|
# emulator. CI is the only instrument, so it has to report enough to be
|
|
# conclusive. The first line proves what the guest actually got regardless of
|
|
# the result; the rest runs only on failure, so a green run is unchanged.
|
|
#
|
|
# Each line here is a separate `sh -c` -- the action splits the script on
|
|
# newlines -- so the failure handler has to stay on one line.
|
|
script: |
|
|
adb shell cat /proc/meminfo | grep -E 'MemTotal|MemAvailable|SwapTotal'
|
|
./gradlew :app:connectedDebugAndroidTest -PabiFilters=x86_64 || { echo "=== guest memory at failure ==="; adb shell cat /proc/meminfo | grep -E 'MemTotal|MemAvailable|SwapTotal'; echo "=== kernel OOM kills ==="; adb shell dmesg | grep -iE 'oom|lowmemory|killed process' | tail -20; echo "=== native crashes ==="; adb logcat -d -b crash | tail -40; exit 1; }
|
|
|
|
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
if: always()
|
|
with:
|
|
name: e2e-report-api${{ matrix.label }}
|
|
path: |
|
|
app/build/reports/androidTests/
|
|
app/build/outputs/androidTest-results/
|