The custom-provisioned API 37 preview emulator has been stable, so fold its E2E job into the aggregating "CI passed" gate's needs. Because branch protection requires only that single check, no settings change is needed. Drop the now-inaccurate "non-blocking" wording from the job name and comments. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
306 lines
13 KiB
YAML
306 lines
13 KiB
YAML
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
name: CI
|
|
|
|
on:
|
|
pull_request:
|
|
branches: [main]
|
|
|
|
# A new push to a PR cancels any in-flight run for that PR.
|
|
concurrency:
|
|
group: ci-${{ github.ref }}
|
|
cancel-in-progress: true
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
env:
|
|
# SDK packages this project builds against (compileSdk 37 / build-tools 37.0.0).
|
|
# Quote the package ids when passed to sdkmanager — the ';' is a shell separator.
|
|
ANDROID_PLATFORM: "platforms;android-37.0"
|
|
ANDROID_BUILD_TOOLS: "build-tools;37.0.0"
|
|
|
|
jobs:
|
|
debug-build:
|
|
name: Debug build
|
|
# x86_64: Linux-arm64 runners can't set up this SDK — android-actions/setup-android's sdkmanager
|
|
# fails (exit 1) on the android-37.0 preview platform, and the emulator package has no arm64-Linux
|
|
# build. Build/unit-test results are host-arch-independent anyway (R8/AGP/JVM); real arm64
|
|
# device-ABI coverage would need arm64 emulators, which require macOS hosts.
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- name: Check out source
|
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
|
|
- name: Set up JDK 21
|
|
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
|
|
with:
|
|
distribution: temurin
|
|
java-version: "21"
|
|
|
|
- name: Set up Android SDK
|
|
uses: android-actions/setup-android@40fd30fb8d7440372e1316f5d1809ec01dcd3699 # v4.0.1
|
|
|
|
- name: Install SDK platform and build-tools
|
|
run: sdkmanager "$ANDROID_PLATFORM" "$ANDROID_BUILD_TOOLS"
|
|
|
|
- name: Set up Gradle
|
|
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
|
|
|
|
- name: Assemble debug APK
|
|
run: ./gradlew assembleDebug --stacktrace
|
|
|
|
- name: Upload debug APK
|
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
with:
|
|
name: debug-apk
|
|
path: app/build/outputs/apk/debug/*.apk
|
|
if-no-files-found: error
|
|
|
|
unit-tests:
|
|
name: Unit tests
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- name: Check out source
|
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
|
|
- name: Set up JDK 21
|
|
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
|
|
with:
|
|
distribution: temurin
|
|
java-version: "21"
|
|
|
|
- name: Set up Android SDK
|
|
uses: android-actions/setup-android@40fd30fb8d7440372e1316f5d1809ec01dcd3699 # v4.0.1
|
|
|
|
- name: Install SDK platform and build-tools
|
|
run: sdkmanager "$ANDROID_PLATFORM" "$ANDROID_BUILD_TOOLS"
|
|
|
|
- name: Set up Gradle
|
|
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
|
|
|
|
- name: Run unit tests
|
|
run: ./gradlew testDebugUnitTest --stacktrace
|
|
|
|
- name: Upload unit test report
|
|
if: ${{ !cancelled() }}
|
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
with:
|
|
name: unit-test-report
|
|
path: app/build/reports/tests/testDebugUnitTest/
|
|
if-no-files-found: warn
|
|
|
|
e2e:
|
|
name: E2E
|
|
runs-on: ubuntu-latest
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
# Every Android API level across the rolling ~7-year support window: minSdk (29 / Android 10,
|
|
# 2019) through the latest stable. Each level boots its own emulator and runs the full
|
|
# instrumented + Compose UI (E2E) suite; all of them fan in to the "CI passed" gate. When a
|
|
# new Android ships, add it and drop the oldest level that has aged out of ~7 years. API 37
|
|
# (preview) is NOT in this matrix because emulator-runner can't provision its nonstandard
|
|
# android-37.0 / google_apis_ps16k image (it would wedge the gate) — it's covered separately
|
|
# by the custom-provisioned `e2e-preview` job below. Keep in sync with
|
|
# testOptions.managedDevices in app/build.gradle.kts.
|
|
api-level: [29, 30, 31, 32, 33, 34, 35, 36]
|
|
steps:
|
|
- name: Check out source
|
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
|
|
- name: Set up JDK 21
|
|
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
|
|
with:
|
|
distribution: temurin
|
|
java-version: "21"
|
|
|
|
- name: Set up Android SDK
|
|
uses: android-actions/setup-android@40fd30fb8d7440372e1316f5d1809ec01dcd3699 # v4.0.1
|
|
|
|
- name: Install SDK platform and build-tools
|
|
run: sdkmanager "$ANDROID_PLATFORM" "$ANDROID_BUILD_TOOLS"
|
|
|
|
- name: Set up Gradle
|
|
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
|
|
|
|
# The hardware-accelerated emulator needs KVM, which is gated behind a udev rule.
|
|
- 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: Cache AVD snapshot
|
|
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
|
id: avd-cache
|
|
with:
|
|
path: |
|
|
~/.android/avd/*
|
|
~/.android/adb*
|
|
key: avd-${{ matrix.api-level }}-google_apis-x86_64
|
|
|
|
# On a cache miss, cold-boot the emulator once so its snapshot can be cached,
|
|
# making subsequent runs start from a warm snapshot.
|
|
- name: Create AVD and generate snapshot for caching
|
|
if: steps.avd-cache.outputs.cache-hit != 'true'
|
|
uses: reactivecircus/android-emulator-runner@e89f39f1abbbd05b1113a29cf4db69e7540cae5a # v2.37.0
|
|
with:
|
|
api-level: ${{ matrix.api-level }}
|
|
target: google_apis
|
|
arch: x86_64
|
|
force-avd-creation: false
|
|
emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
|
|
disable-animations: false
|
|
script: echo "Generated AVD snapshot for caching."
|
|
|
|
- name: Run E2E tests
|
|
uses: reactivecircus/android-emulator-runner@e89f39f1abbbd05b1113a29cf4db69e7540cae5a # v2.37.0
|
|
with:
|
|
api-level: ${{ matrix.api-level }}
|
|
target: google_apis
|
|
arch: x86_64
|
|
force-avd-creation: false
|
|
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
|
|
disable-animations: true
|
|
script: ./gradlew connectedDebugAndroidTest --stacktrace
|
|
|
|
- name: Upload E2E test report
|
|
if: ${{ !cancelled() }}
|
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
with:
|
|
name: e2e-test-report-api${{ matrix.api-level }}
|
|
path: app/build/reports/androidTests/connected/
|
|
if-no-files-found: warn
|
|
|
|
# API 37 (Android 17, preview) E2E. Its only system image is the nonstandard
|
|
# android-37.0 / google_apis_ps16k (16 KB page size), which reactivecircus/android-emulator-runner
|
|
# can't provision (it builds android-37 / google_apis, neither of which exists), so this job
|
|
# CUSTOM-PROVISIONS the emulator with sdkmanager/avdmanager/emulator directly. It is REQUIRED:
|
|
# part of the "CI passed" gate's needs (the preview emulator has proven stable in practice), so a
|
|
# genuine failure blocks merges. When a stable, emulator-runner-friendly API 37 image ships, fold
|
|
# 37 into the main `e2e` matrix and delete this job.
|
|
e2e-preview:
|
|
name: E2E (API 37 preview)
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 35
|
|
env:
|
|
API37_IMAGE: "system-images;android-37.0;google_apis_ps16k;x86_64"
|
|
steps:
|
|
- name: Check out source
|
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
|
|
- name: Set up JDK 21
|
|
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
|
|
with:
|
|
distribution: temurin
|
|
java-version: "21"
|
|
|
|
- name: Set up Android SDK
|
|
uses: android-actions/setup-android@40fd30fb8d7440372e1316f5d1809ec01dcd3699 # v4.0.1
|
|
|
|
- name: Set up Gradle
|
|
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
|
|
|
|
# The hardware-accelerated emulator needs KVM, which is gated behind a udev rule.
|
|
- 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
|
|
|
|
# Cache the ~1 GB preview system image so only the first run pays the download.
|
|
- name: Cache API 37 system image
|
|
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
|
with:
|
|
# GitHub-hosted ubuntu runners install the SDK at /usr/local/lib/android/sdk; caching the
|
|
# image dir (with its package metadata) lets sdkmanager treat it as installed and skip the
|
|
# re-download on a cache hit.
|
|
path: /usr/local/lib/android/sdk/system-images/android-37.0
|
|
key: sysimg-android-37.0-google_apis_ps16k-x86_64
|
|
|
|
- name: Install SDK packages + preview system image
|
|
run: sdkmanager "$ANDROID_PLATFORM" "$ANDROID_BUILD_TOOLS" "platform-tools" "emulator" "$API37_IMAGE"
|
|
|
|
- name: Create API 37 AVD
|
|
run: |
|
|
# avdmanager and the emulator disagree on the default AVD dir when ANDROID_SDK_HOME is set
|
|
# on the runner (avdmanager writes $ANDROID_SDK_HOME/.android/avd; the emulator looks in
|
|
# $ANDROID_SDK_HOME/avd), which made the boot step report "Unknown AVD name [api37]". Pin
|
|
# ANDROID_AVD_HOME so both agree, and carry it to the boot step via $GITHUB_ENV.
|
|
export ANDROID_AVD_HOME="$HOME/.android/avd"
|
|
echo "ANDROID_AVD_HOME=$ANDROID_AVD_HOME" >> "$GITHUB_ENV"
|
|
mkdir -p "$ANDROID_AVD_HOME"
|
|
echo "no" | avdmanager create avd -n api37 -k "$API37_IMAGE" -d pixel_2 --force
|
|
echo "AVDs visible to the emulator:"; "$ANDROID_SDK_ROOT/emulator/emulator" -list-avds
|
|
|
|
- name: Boot emulator and run E2E
|
|
run: |
|
|
set -euo pipefail
|
|
EMU_LOG="${RUNNER_TEMP:-/tmp}/emulator.log"
|
|
|
|
boot_emulator() {
|
|
echo "::group::Start API 37 emulator (attempt $1)"
|
|
# Capture the emulator's own output — without this a boot failure is invisible.
|
|
"$ANDROID_SDK_ROOT/emulator/emulator" -avd api37 \
|
|
-no-window -no-audio -no-boot-anim -no-snapshot -accel on \
|
|
-gpu swiftshader_indirect -camera-back none -camera-front none > "$EMU_LOG" 2>&1 &
|
|
# ONE bounded wait covering both device registration and full boot, so a stuck emulator
|
|
# fails fast instead of hanging the whole job until the 35-min cap (the original bug).
|
|
if timeout 300 adb wait-for-device shell \
|
|
'while [ "$(getprop sys.boot_completed | tr -d "\r")" != "1" ]; do sleep 2; done'; then
|
|
echo "::endgroup::"; return 0
|
|
fi
|
|
echo "::endgroup::"
|
|
echo "::warning::API 37 emulator did not boot within 300s (attempt $1)"
|
|
adb devices || true
|
|
echo "--- emulator.log (tail) ---"; tail -120 "$EMU_LOG" || true
|
|
adb emu kill 2>/dev/null || true
|
|
sleep 5
|
|
return 1
|
|
}
|
|
|
|
booted=0
|
|
for attempt in 1 2; do boot_emulator "$attempt" && { booted=1; break; }; done
|
|
[ "$booted" = "1" ] || { echo "::error::API 37 preview emulator failed to boot after 2 attempts"; exit 1; }
|
|
|
|
adb shell input keyevent 82 || true
|
|
./gradlew connectedDebugAndroidTest --stacktrace
|
|
|
|
- name: Dump emulator log on failure
|
|
if: failure()
|
|
run: |
|
|
echo "--- emulator.log ---"; tail -200 "${RUNNER_TEMP:-/tmp}/emulator.log" 2>/dev/null || echo "(none)"
|
|
echo "--- logcat ---"; adb logcat -d 2>/dev/null | tail -120 || echo "(device unavailable)"
|
|
|
|
- name: Shut down emulator
|
|
if: always()
|
|
run: adb emu kill || true
|
|
|
|
- name: Upload E2E (API 37) report
|
|
if: ${{ !cancelled() }}
|
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
with:
|
|
name: e2e-test-report-api37-preview
|
|
path: app/build/reports/androidTests/connected/
|
|
if-no-files-found: warn
|
|
|
|
# Single aggregating gate so branch protection can require ALL CI jobs with one stable status
|
|
# check. It depends on every job — including each api-level of the E2E matrix — so adding/removing
|
|
# a matrix level needs no change to branch protection (the per-"(api-level)" check names would
|
|
# otherwise have to be re-listed each time).
|
|
ci-passed:
|
|
name: CI passed
|
|
if: always()
|
|
needs: [debug-build, unit-tests, e2e, e2e-preview]
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- name: Verify every required job succeeded
|
|
if: ${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}
|
|
run: |
|
|
echo "Required CI jobs did not all succeed:"
|
|
echo " debug-build: ${{ needs.debug-build.result }}"
|
|
echo " unit-tests: ${{ needs.unit-tests.result }}"
|
|
echo " e2e: ${{ needs.e2e.result }}"
|
|
echo " e2e-preview: ${{ needs.e2e-preview.result }}"
|
|
exit 1
|