From 54d6e9c57b1fee57bb3e86d9b653b39bf7c50c76 Mon Sep 17 00:00:00 2001 From: Jason Ross Date: Thu, 20 Aug 2026 13:08:06 -0500 Subject: [PATCH] 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) --- .github/workflows/build.yml | 6 +- .github/workflows/status_check.yml | 184 +++++++++++++++++++++++++++++ 2 files changed, 189 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/status_check.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 60536f5..bf2b0f6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -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: diff --git a/.github/workflows/status_check.yml b/.github/workflows/status_check.yml new file mode 100644 index 0000000..e400e26 --- /dev/null +++ b/.github/workflows/status_check.yml @@ -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/