From 4cf36c046381921f205cf54b3b9730e486d49643 Mon Sep 17 00:00:00 2001 From: Siubeng Date: Fri, 16 Oct 2026 09:01:07 +0000 Subject: [PATCH 1/2] ci: cache Android toolchains and emulator snapshots Reuse the Ubuntu 24.04 SDK, install only missing versioned packages, and manage Gradle task caching with setup-gradle. Reuse clean API 28/36 emulator snapshots and system images, and cancel superseded runs. Cover cold, partial, archive-based, warm, and failed SDK setup paths. --- .github/scripts/install-android-packages.sh | 24 +++++ .github/workflows/debug-build.yml | 102 ++++++++++++++++++-- test/ci-android-toolchain.test.js | 79 +++++++++++++++ 3 files changed, 196 insertions(+), 9 deletions(-) create mode 100644 .github/scripts/install-android-packages.sh create mode 100644 test/ci-android-toolchain.test.js diff --git a/.github/scripts/install-android-packages.sh b/.github/scripts/install-android-packages.sh new file mode 100644 index 0000000..0a3db9f --- /dev/null +++ b/.github/scripts/install-android-packages.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Runner images and restored caches may already contain some or all required packages. +# Use the local SDK metadata instead of making sdkmanager refresh remote indexes every run. +: "${ANDROID_HOME:?ANDROID_HOME must point to the Android SDK}" +if (( $# == 0 )); then + echo 'Pass at least one SDK package ID.' >&2 + exit 1 +fi + +missing=() +for package in "$@"; do + directory="${ANDROID_HOME}/${package//;/\/}" + if [[ -f "$directory/package.xml" || -f "$directory/source.properties" ]]; then + echo "Using installed SDK package: $package" + else + missing+=("$package") + fi +done + +if (( ${#missing[@]} )); then + sdkmanager --sdk_root="$ANDROID_HOME" --install "${missing[@]}" +fi diff --git a/.github/workflows/debug-build.yml b/.github/workflows/debug-build.yml index 0b86660..0a494d9 100644 --- a/.github/workflows/debug-build.yml +++ b/.github/workflows/debug-build.yml @@ -9,9 +9,19 @@ on: - '**.md' workflow_dispatch: +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +env: + ANDROID_API_LEVEL: '36' + ANDROID_BUILD_TOOLS: '36.0.0' + ANDROID_NDK_VERSION: '28.2.13676358' + ANDROID_CMAKE_VERSION: '3.22.1' + jobs: build: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 @@ -21,18 +31,43 @@ jobs: with: java-version: '25' distribution: temurin - cache: gradle - - name: Set up Android SDK - uses: android-actions/setup-android@v4 + - name: Set up Gradle cache + uses: gradle/actions/setup-gradle@v5 + with: + cache-read-only: ${{ github.event_name == 'pull_request' }} + + - name: Locate preinstalled Android SDK + id: android-sdk + run: | + test -d "$ANDROID_HOME/cmdline-tools/latest/bin" + echo "$ANDROID_HOME/cmdline-tools/latest/bin" >> "$GITHUB_PATH" + echo "$ANDROID_HOME/platform-tools" >> "$GITHUB_PATH" + echo "root=$ANDROID_HOME" >> "$GITHUB_OUTPUT" + + - name: Cache required Android toolchain + uses: actions/cache@v4 with: - packages: platform-tools + path: | + ${{ steps.android-sdk.outputs.root }}/platforms/android-${{ env.ANDROID_API_LEVEL }} + ${{ steps.android-sdk.outputs.root }}/build-tools/${{ env.ANDROID_BUILD_TOOLS }} + ${{ steps.android-sdk.outputs.root }}/ndk/${{ env.ANDROID_NDK_VERSION }} + ${{ steps.android-sdk.outputs.root }}/cmake/${{ env.ANDROID_CMAKE_VERSION }} + key: android-toolchain-v1-ubuntu24-${{ runner.arch }}-api${{ env.ANDROID_API_LEVEL }}-bt${{ env.ANDROID_BUILD_TOOLS }}-ndk${{ env.ANDROID_NDK_VERSION }}-cmake${{ env.ANDROID_CMAKE_VERSION }} - - name: Install Android build tools - run: sdkmanager 'platforms;android-36' 'build-tools;36.0.0' 'ndk;28.2.13676358' 'cmake;3.22.1' + - name: Verify SDK installation helper + run: node --test --test-isolation=none test/ci-android-toolchain.test.js + + - name: Install missing Android packages + run: | + bash .github/scripts/install-android-packages.sh \ + "platforms;android-$ANDROID_API_LEVEL" \ + "build-tools;$ANDROID_BUILD_TOOLS" \ + "ndk;$ANDROID_NDK_VERSION" \ + "cmake;$ANDROID_CMAKE_VERSION" - name: Build and verify Debug - run: ./gradlew --no-daemon :shared:testDebugUnitTest :common:testDebugUnitTest :common:assembleDebugAndroidTest :mobile:lintDebug :mobile:assembleDebug + run: ./gradlew --no-daemon --build-cache :shared:testDebugUnitTest :common:testDebugUnitTest :common:assembleDebugAndroidTest :mobile:lintDebug :mobile:assembleDebug - name: Upload browser runtime test APK uses: actions/upload-artifact@v4 @@ -60,13 +95,33 @@ jobs: browser-runtime: name: Browser HTTP on Android ${{ matrix.api-level }} needs: build - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 timeout-minutes: 20 strategy: fail-fast: false matrix: api-level: [28, 36] steps: + - name: Locate emulator cache directories + id: emulator-cache + run: | + echo "sdk=$ANDROID_HOME" >> "$GITHUB_OUTPUT" + echo "image-version=$ImageVersion" >> "$GITHUB_OUTPUT" + + # Cache only this API's image and clean AVD. The runner image version also + # invalidates snapshots when the preinstalled emulator/host software changes. + - name: Restore Android emulator and clean snapshot + id: avd-cache + uses: actions/cache/restore@v4 + with: + path: | + ${{ steps.emulator-cache.outputs.sdk }}/emulator + ${{ steps.emulator-cache.outputs.sdk }}/platforms/android-${{ matrix.api-level }} + ${{ steps.emulator-cache.outputs.sdk }}/system-images/android-${{ matrix.api-level }}/google_apis/x86_64 + ~/.android/avd + ~/.android/adb* + key: android-avd-v1-ubuntu24-${{ runner.arch }}-${{ steps.emulator-cache.outputs.image-version }}-api${{ matrix.api-level }}-google_apis-x86_64 + - name: Download browser runtime test APK uses: actions/download-artifact@v4 with: @@ -76,12 +131,41 @@ jobs: - name: Enable KVM run: sudo chmod a+rw /dev/kvm + - name: Create clean emulator snapshot on cache miss + if: steps.avd-cache.outputs.cache-hit != 'true' + uses: reactivecircus/android-emulator-runner@v2 + with: + api-level: ${{ matrix.api-level }} + arch: x86_64 + target: google_apis + avd-name: browser-runtime + force-avd-creation: false + disable-animations: false + emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none + script: echo 'Clean emulator snapshot created.' + + # Save before installing or running tests so later jobs restore a clean device. + - name: Save clean emulator snapshot + if: steps.avd-cache.outputs.cache-hit != 'true' + uses: actions/cache/save@v4 + with: + path: | + ${{ steps.emulator-cache.outputs.sdk }}/emulator + ${{ steps.emulator-cache.outputs.sdk }}/platforms/android-${{ matrix.api-level }} + ${{ steps.emulator-cache.outputs.sdk }}/system-images/android-${{ matrix.api-level }}/google_apis/x86_64 + ~/.android/avd + ~/.android/adb* + key: ${{ steps.avd-cache.outputs.cache-primary-key }} + - name: Verify localized pages on Android ICU uses: reactivecircus/android-emulator-runner@v2 with: api-level: ${{ matrix.api-level }} arch: x86_64 target: google_apis + avd-name: browser-runtime + 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: | adb install -r runtime-tests/common-debug-androidTest.apk diff --git a/test/ci-android-toolchain.test.js b/test/ci-android-toolchain.test.js new file mode 100644 index 0000000..4157194 --- /dev/null +++ b/test/ci-android-toolchain.test.js @@ -0,0 +1,79 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, existsSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { spawnSync } from 'node:child_process'; + +const script = fileURLToPath(new URL('../.github/scripts/install-android-packages.sh', import.meta.url)); +const packages = ['platforms;android-36', 'build-tools;36.0.0', 'ndk;28.2.13676358', 'cmake;3.22.1']; + +function sdkFixture(t) { + // Include spaces to check SDK paths and package arguments are kept intact. + const directory = mkdtempSync(join(tmpdir(), 'wheelplay sdk ')); + t.after(() => rmSync(directory, { recursive: true, force: true })); + const sdk = join(directory, 'Android SDK'), bin = join(directory, 'bin'), log = join(directory, 'sdkmanager.log'); + mkdirSync(sdk); mkdirSync(bin); + writeFileSync(join(bin, 'sdkmanager'), `#!/usr/bin/env bash +set -euo pipefail +printf '%s\\n' "$@" > "$SDK_CALL_LOG" +if [[ "\${MOCK_SDKMANAGER_FAIL:-0}" == 1 ]]; then exit 7; fi +shift 2 +for package in "$@"; do + directory="$ANDROID_HOME/\${package//;/\\/}" + mkdir -p "$directory" + printf 'installed' > "$directory/package.xml" +done +`, { mode: 0o755 }); + return { + sdk, log, + installed(packageId) { + const directory = join(sdk, ...packageId.split(';')); + mkdirSync(directory, { recursive: true }); + writeFileSync(join(directory, 'package.xml'), 'preinstalled'); + }, + run(fail = false) { + return spawnSync('bash', [script, ...packages], { + encoding: 'utf8', + env: { ...process.env, PATH: `${bin}:${process.env.PATH}`, ANDROID_HOME: sdk, + SDK_CALL_LOG: log, MOCK_SDKMANAGER_FAIL: fail ? '1' : '0' }, + }); + }, + calls() { return readFileSync(log, 'utf8').trimEnd().split('\n'); }, + }; +} + +test('SDK cold start installs all packages and a warm run skips sdkmanager entirely', t => { + const f = sdkFixture(t); + assert.equal(f.run().status, 0); + assert.deepEqual(f.calls(), [`--sdk_root=${f.sdk}`, '--install', ...packages]); + rmSync(f.log); + assert.equal(f.run().status, 0); + assert.equal(existsSync(f.log), false); +}); + +test('a partial SDK cache installs only missing packages and preserves preinstalled metadata', t => { + const f = sdkFixture(t); + f.installed(packages[0]); f.installed(packages[1]); + assert.equal(f.run().status, 0); + assert.deepEqual(f.calls(), [`--sdk_root=${f.sdk}`, '--install', ...packages.slice(2)]); + assert.equal(readFileSync(join(f.sdk, 'platforms', 'android-36', 'package.xml'), 'utf8'), 'preinstalled'); +}); + +test('SDK packages unpacked from official archives do not require sdkmanager metadata', t => { + const f = sdkFixture(t); + for (const packageId of packages) { + const directory = join(f.sdk, ...packageId.split(';')); + mkdirSync(directory, { recursive: true }); + writeFileSync(join(directory, 'source.properties'), 'Pkg.Revision = 1'); + } + assert.equal(f.run().status, 0); + assert.equal(existsSync(f.log), false); +}); + +test('sdkmanager failures fail the setup step without marking missing packages installed', t => { + const f = sdkFixture(t); + assert.equal(f.run(true).status, 7); + assert.equal(existsSync(join(f.sdk, 'ndk', '28.2.13676358', 'package.xml')), false); +}); From 54101347963eae83046f52f7f49c5b97569b9982 Mon Sep 17 00:00:00 2001 From: Siubeng Date: Fri, 16 Oct 2026 09:03:34 +0000 Subject: [PATCH 2/2] ci: run SDK helper tests with supported Node 22 options --- .github/workflows/debug-build.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/debug-build.yml b/.github/workflows/debug-build.yml index 0a494d9..4bc513c 100644 --- a/.github/workflows/debug-build.yml +++ b/.github/workflows/debug-build.yml @@ -26,6 +26,11 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Set up Node.js 22 + uses: actions/setup-node@v4 + with: + node-version: '22' + - name: Set up JDK 25 uses: actions/setup-java@v4 with: @@ -56,7 +61,7 @@ jobs: key: android-toolchain-v1-ubuntu24-${{ runner.arch }}-api${{ env.ANDROID_API_LEVEL }}-bt${{ env.ANDROID_BUILD_TOOLS }}-ndk${{ env.ANDROID_NDK_VERSION }}-cmake${{ env.ANDROID_CMAKE_VERSION }} - name: Verify SDK installation helper - run: node --test --test-isolation=none test/ci-android-toolchain.test.js + run: node --test test/ci-android-toolchain.test.js - name: Install missing Android packages run: |