Add a pull-request status check across API 33-37

Runs the JVM tests and the instrumented suite on every pull request to main,
with one emulator job per supported API level.

The matrix is the whole range rather than a single level because the
foreground-service type differs across it -- none below 34, dataSync at 34,
mediaProcessing from 35 -- so testing one level would leave two thirds of that
branch unexercised. Running the range locally is what caught a test that had
baked in an assumption about the host's encoders.

API 37 needs its image level stated separately. It is published as
android-37.0, not android-37, so a plain integer resolves to nothing and the
image download silently finds no package.

FFmpeg is built once and shared. The AAR is not committed -- 35 MB of native
code, and F-Droid strips checked-in binaries -- but every job needs it, since
the app compiles against it and the instrumented tests exercise it for real.
Building it is a full cross-compile of FFmpeg, x264, x265 and SVT-AV1, so it
is cached on the contents of tools/ffmpeg, which is what actually determines
the output. The job also asserts the AAR carries native libraries for both
ABIs: a truncated or stub archive would otherwise pass a file-exists check and
send the matrix off to fail confusingly five times over.

fail-fast is off. Knowing whether a failure is universal or specific to one
API level is most of the diagnosis.

build.yml no longer runs on pull requests. It triggered on every PR with no
branch filter, so both workflows would have run, and its unit job falls back
to a stub AAR -- a weaker check that could mask a compile break the real one
would catch. It keeps its post-merge and release duties.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-20 13:08:06 -05:00
co-authored by Claude Opus 5
parent a172cb137f
commit 54d6e9c57b
2 changed files with 189 additions and 1 deletions
+5 -1
View File
@@ -1,9 +1,13 @@
name: Build
# Pull requests are covered by status_check.yml, which builds the real FFmpeg AAR and
# runs the instrumented suite across API 33-37. This workflow keeps the post-merge and
# release duties, and deliberately does not duplicate PR validation: its unit job falls
# back to a stub AAR, which is a weaker check than the one status_check.yml performs.
on:
push:
branches: [main]
pull_request:
tags: ['v*']
jobs:
test:
+184
View File
@@ -0,0 +1,184 @@
name: Status check
on:
pull_request:
branches: [main]
# A newer push to the same PR makes the in-flight run obsolete. Emulator matrices are
# expensive, so cancel rather than let them pile up.
concurrency:
group: status-check-${{ github.ref }}
cancel-in-progress: true
env:
# Must match the coordinate app/build.gradle.kts loads from app/libs/.
FFMPEG_AAR: ffmpeg-kit-next-8.1.1.aar
jobs:
# ---------------------------------------------------------------------------
# FFmpeg is not committed: the AAR is ~35 MB of native code, and F-Droid strips
# checked-in binaries. Every other job needs it, because the app module compiles
# against it and the instrumented tests exercise it for real.
#
# Building it is a full cross-compile of FFmpeg, x264, x265 and SVT-AV1, so it is
# cached on the contents of tools/ffmpeg. That directory pins the upstream tag and
# the configure flags, which is exactly what determines the output.
# ---------------------------------------------------------------------------
ffmpeg:
name: FFmpeg AAR
runs-on: ubuntu-latest
timeout-minutes: 120
steps:
- uses: actions/checkout@v4
- name: Restore cached AAR
id: cache
uses: actions/cache@v4
with:
path: tools/ffmpeg/out
key: ffmpeg-aar-${{ hashFiles('tools/ffmpeg/Containerfile', 'tools/ffmpeg/build-ffmpeg.sh') }}
# The Nix store plus the build tree runs to several gigabytes, which does not fit
# alongside the runner's preinstalled toolchains.
- name: Free disk space
if: steps.cache.outputs.cache-hit != 'true'
run: |
sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /usr/local/share/boost
sudo docker image prune -af || true
df -h /
- name: Build the AAR
if: steps.cache.outputs.cache-hit != 'true'
working-directory: tools/ffmpeg
run: |
mkdir -p out
docker build -t ffmpeg-kit-builder:ci -f Containerfile .
docker run --rm -v "$PWD/out":/work/out ffmpeg-kit-builder:ci full
- name: Check the AAR is present and plausibly complete
run: |
AAR=$(find tools/ffmpeg/out -name 'ffmpeg-kit-next*.aar' | head -1)
test -n "$AAR" || { echo "::error::no AAR produced"; exit 1; }
# A truncated or stub AAR would still satisfy `test -f`, so check it carries
# native libraries for both ABIs before letting the matrix depend on it.
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
cp "$AAR" "${{ env.FFMPEG_AAR }}"
- uses: actions/upload-artifact@v4
with:
name: ffmpeg-aar
path: ${{ env.FFMPEG_AAR }}
retention-days: 1
# ---------------------------------------------------------------------------
# JVM tests: the routing matrix, the FFmpeg argument builder, the concat planner
# and the retry rule. These need no device and are the fastest signal on a PR.
# ---------------------------------------------------------------------------
unit:
name: Unit tests
runs-on: ubuntu-latest
needs: ffmpeg
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
with:
name: ffmpeg-aar
path: app/libs/
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '17' # AGP 9 will not run on anything older
- uses: gradle/actions/setup-gradle@v4
- run: ./gradlew :app:testDebugUnitTest
- uses: actions/upload-artifact@v4
if: always()
with:
name: unit-test-report
path: app/build/reports/tests/
# ---------------------------------------------------------------------------
# Instrumented tests across the whole supported range. minSdk is 33 and targetSdk
# is 37, and the foreground-service type differs across that range -- none below
# 34, dataSync at 34, mediaProcessing from 35 -- so a single API level would leave
# two thirds of that branch unexercised.
# ---------------------------------------------------------------------------
instrumented:
name: E2E API ${{ matrix.api-level }}
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:
- api-level: 33
system-image-api-level: 33
- api-level: 34
system-image-api-level: 34
- api-level: 35
system-image-api-level: 35
- api-level: 36
system-image-api-level: 36
# API 37 is published as android-37.0, not android-37, so the image level
# has to be given separately or the download resolves to nothing.
- api-level: 37
system-image-api-level: "37.0"
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
with:
name: ffmpeg-aar
path: app/libs/
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '17'
- uses: gradle/actions/setup-gradle@v4
# 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@v2
with:
api-level: ${{ matrix.api-level }}
system-image-api-level: ${{ matrix.system-image-api-level }}
target: google_apis
arch: x86_64
profile: pixel_6
# -gpu swiftshader_indirect is the usual CI choice. It 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
script: ./gradlew :app:connectedDebugAndroidTest
- uses: actions/upload-artifact@v4
if: always()
with:
name: e2e-report-api${{ matrix.api-level }}
path: |
app/build/reports/androidTests/
app/build/outputs/androidTest-results/