Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
9641bc8
chore(thumbjar): move `apps/android-ime` into `packages/thumbjar` and…
christianhg Oct 31, 2026
1574049
chore(thumbjar): format the modules that sat under the app's ignored …
christianhg Oct 31, 2026
6caf0e6
chore(thumbjar): add a package that types on real Gboard on an emulator
christianhg Oct 31, 2026
aaac59f
chore(thumbjar): lint thumbjar with the rules for `packages/`
christianhg Oct 31, 2026
a698bea
test(editor): run the Android device tests from `packages/editor` wit…
christianhg Oct 31, 2026
5b00618
docs(thumbjar): add a logo to the README
christianhg Oct 31, 2026
6c5be3a
feat(thumbjar): type into `textarea` and text `input` fields
christianhg Nov 1, 2026
3b5e0c3
test(editor): add a `textarea` control page for the Android device tests
christianhg Nov 1, 2026
b9362d3
feat(thumbjar): drive a case of `compose`, `enter` and `backspace` st…
christianhg Nov 1, 2026
69c0eb1
feat(thumbjar): derive a device test file's cap from its number of tests
christianhg Nov 1, 2026
f7b17e7
ci: run each Android device test file in its own job, and retry a los…
christianhg Nov 1, 2026
29c8082
feat(thumbjar): block Play Store updates and wait for Gboard's Japane…
christianhg Nov 2, 2026
a01df8c
ci: give each Android device job its prep's cap and a minute of headr…
christianhg Nov 2, 2026
b8e2fea
ci(thumbjar): stop deleting Chrome's saved tabs, which the emulator n…
christianhg Nov 3, 2026
fbf00ea
ci: shrink the Android device job caps to thumbjar's shorter Chrome c…
christianhg Nov 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 72 additions & 49 deletions .github/workflows/android-ime.yml
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
name: Android IME

# Types on real Gboard in an Android emulator into a test page built from
# the workspace editor, see `apps/android-ime/README.md`.
# the workspace editor, see `packages/editor/android/README.md`.
#
# Each job prepares the emulator and runs the Vitest device tests for its
# language. A job is red when the device never booted, a prep step failed
# or a test failed, an editor assertion included. This workflow is not a
# required check, so a red job blocks nothing.
# Each job prepares a fresh emulator for its row's language and runs that
# row's device test file. A job is red when a prep step failed or a test
# failed, an editor assertion included, or as an infrastructure failure when
# the emulator never booted or adb lost the device on the retry too. This
# workflow is not a required check, so a red job blocks nothing.

on:
pull_request:
Expand All @@ -17,7 +18,10 @@ on:
- packages/editor/src/engine/dom/plugin/dom-editor.ts
- packages/editor/src/behaviors/behavior.abstract.input.ts
- packages/editor/src/editor/Editable.tsx
- apps/android-ime/**
- packages/editor/package.json
- packages/editor/android/**
- packages/editor/vitest.android.config.ts
- packages/thumbjar/**
- .github/workflows/android-ime.yml
schedule:
- cron: "17 3 * * *"
Expand All @@ -32,22 +36,45 @@ permissions:

jobs:
emulator:
name: Android IME (${{ matrix.lang }})
name: Android IME (${{ matrix.suite.file }})
runs-on: ubuntu-latest
# The sum of the step caps below on the boot retry path, see the budget in
# `apps/android-ime/README.md`.
timeout-minutes: 153
# The row's job cap: the sum of the step caps below on the retry path and
# a minute for the runner between steps, see the budget in
# `packages/editor/android/README.md`. `android/scripts/budget.test.ts`
# checks it.
timeout-minutes: ${{ matrix.suite.job_minutes }}
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ vars.TURBO_TEAM }}

strategy:
fail-fast: false
matrix:
lang: [ja, en]
# One row per device test file CI runs, on a device prepared for its
# language, with its run steps' and its job's caps, which the
# language's prep sets. `tests/en-textarea.test.ts` is a control run
# by hand.
suite:
- {
lang: ja,
file: tests/ja.test.ts,
run_minutes: 56,
job_minutes: 161,
}
- {
lang: en,
file: tests/en.test.ts,
run_minutes: 52,
job_minutes: 153,
}
api: [36]

steps:
- name: Name the suite
id: suite
timeout-minutes: 1
run: echo "name=$(basename '${{ matrix.suite.file }}' .test.ts)" >> "$GITHUB_OUTPUT"

- uses: actions/checkout@v7
timeout-minutes: 3

Expand All @@ -69,7 +96,6 @@ jobs:
- uses: actions/setup-node@v6
timeout-minutes: 3
with:
# The driver runs its `.ts` files with Node 24's type stripping.
node-version: 24

- name: Install project dependencies
Expand All @@ -79,14 +105,13 @@ jobs:
- name: Build the test page
timeout-minutes: 10
run: |
pnpm exec turbo build --filter='android-ime^...'
pnpm --filter android-ime build:page
pnpm exec turbo build --filter='@portabletext/editor...'
pnpm --filter @portabletext/editor build:android
node -e "console.log('EDITOR_VERSION', require('./packages/editor/package.json').version)"

- name: Fetch the uiautomator2 server jar
timeout-minutes: 2
working-directory: apps/android-ime
run: node scripts/fetch-u2-jar.ts
run: pnpm --filter @portabletext/editor exec thumbjar fetch-u2-jar

# The emulator steps install the same packages again, which only checks them once they are here.
- name: Install the emulator and its system image
Expand All @@ -97,16 +122,16 @@ jobs:

- name: Record start
timeout-minutes: 1
working-directory: apps/android-ime
run: mkdir -p out && echo "EMULATOR_START at $(date +%s)" | tee out/timing.txt
working-directory: packages/editor
run: mkdir -p android/out && echo "EMULATOR_START at $(date +%s)" | tee android/out/timing.txt

# Boot, device prep and the tests, see the budget in `apps/android-ime/README.md`.
# Boot, device prep and the tests, see the budget in `packages/editor/android/README.md`.
- name: Run on emulator
id: run1
continue-on-error: true
timeout-minutes: 52
timeout-minutes: ${{ matrix.suite.run_minutes }}
env:
IME_LANG: ${{ matrix.lang }}
IME_LANG: ${{ matrix.suite.lang }}
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: ${{ matrix.api }}
Expand All @@ -119,25 +144,26 @@ jobs:
heap-size: 512M
emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim
emulator-boot-timeout: 360
working-directory: apps/android-ime
script: scripts/run-on-device.sh

# The emulator sometimes never comes online. Retry once, but only when it never booted.
- name: Check boot
id: boot
working-directory: packages/editor
script: android/run-on-device.sh ${{ matrix.suite.file }}

# The emulator sometimes never comes online, or adb loses it after boot.
# Retry once on a fresh emulator then, never after a failure on a
# reachable device.
- name: Decide the retry
id: retry
if: steps.run1.outcome == 'failure'
timeout-minutes: 1
working-directory: apps/android-ime
run: |
if grep -q BOOTED out/timing.txt; then echo booted=yes >> $GITHUB_OUTPUT; else echo booted=no >> $GITHUB_OUTPUT; echo "BOOT_RETRY"; fi
working-directory: packages/editor
run: node android/scripts/emulator-runs.mjs retry android/out/timing.txt

- name: Run on emulator (boot retry)
- name: Run on emulator (retry)
id: run2
if: steps.run1.outcome == 'failure' && steps.boot.outputs.booted == 'no'
if: steps.run1.outcome == 'failure' && steps.retry.outputs.retry == 'yes'
continue-on-error: true
timeout-minutes: 52
timeout-minutes: ${{ matrix.suite.run_minutes }}
env:
IME_LANG: ${{ matrix.lang }}
IME_LANG: ${{ matrix.suite.lang }}
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: ${{ matrix.api }}
Expand All @@ -150,32 +176,29 @@ jobs:
heap-size: 512M
emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim
emulator-boot-timeout: 360
working-directory: apps/android-ime
script: scripts/run-on-device.sh
working-directory: packages/editor
script: android/run-on-device.sh ${{ matrix.suite.file }}

- name: Boot time
if: always()
timeout-minutes: 1
working-directory: apps/android-ime
working-directory: packages/editor
run: |
s=$(grep EMULATOR_START out/timing.txt | awk '{print $3}')
b=$(grep BOOTED out/timing.txt | awk '{print $3}')
[ -n "$s" ] && [ -n "$b" ] && echo "BOOT_SECONDS $((b - s))"
cat out/phases.log || true
node android/scripts/emulator-runs.mjs boot-seconds android/out/timing.txt
cat android/out/phases.log || true

# The run steps continue on error so the boot retry can follow. This
# step turns the job red when the last run failed.
# The run steps continue on error so the retry can follow. This step
# turns the job red when the last run failed, and names a failure of the
# emulator apart from a test result.
- name: Result
if: always()
timeout-minutes: 1
run: |
if [ "${{ steps.run2.outcome }}" != skipped ]; then outcome="${{ steps.run2.outcome }}"; else outcome="${{ steps.run1.outcome }}"; fi
echo "run outcome: $outcome"
[ "$outcome" = success ]
working-directory: packages/editor
run: node android/scripts/emulator-runs.mjs result android/out/timing.txt "${{ steps.run2.outcome != 'skipped' && steps.run2.outcome || steps.run1.outcome }}"

- uses: actions/upload-artifact@v7
if: always()
timeout-minutes: 5
with:
name: android-ime-${{ matrix.lang }}-api${{ matrix.api }}-${{ github.run_attempt }}
path: apps/android-ime/out/
name: android-ime-${{ steps.suite.outputs.name }}-api${{ matrix.api }}-${{ github.run_attempt }}
path: packages/editor/android/out/
Loading
Loading