- T-iOS-34's stated acceptance is "最大字号不破版" and it was failing: the key bar froze its height at 52pt while an AX5 keycap needs 108.24pt, so caps clipped — recorded as a withKnownIssue rather than fixed. Height now derives from the content size category (and tracks live changes via registerForTraitChanges); the known-issue marker is gone, replaced by positive assertions including a 12-category no-clip sweep and a guard that stays red if anyone writes the constant back. Honest tradeoff: the keycap font is clamped at .accessibility2, the same policy the design system already applies to dense content, because an unclamped AX5 bar would eat the terminal. A test pins the clamp so the two cannot drift. - Thumbnails silently 401'd on a token-gated host: the pipeline built its own transport with no token source, and by design never throws, so every preview degraded to a placeholder with no signal. Assembled from AppEnvironment now. - Android had zero CI while the token wave shipped 24 files of secret-handling code. The instrumented leg is workflow_dispatch-only and says why in the file: no one has ever seen it green on a runner, and a required leg nobody trusts just produces a false green. - Android persisted a validated token before the host probe succeeded, stranding a secret for a host that never paired. App bundle 534 -> 550 on both simulators, zero known issues. Android 687 -> 691.
177 lines
9.2 KiB
YAML
177 lines
9.2 KiB
YAML
# Android CI (F2). Until this file existed the Android client had ZERO CI — 10 modules,
|
|
# 691 JVM tests and the whole `WEBTERM_TOKEN` secret-handling wave (Tink AEAD under an
|
|
# AndroidKeystore master key, the POST /auth probe, cookie stamping on every route and on
|
|
# the WS upgrade) were gated by nothing at all. Modelled on ios.yml: same `on:` shape
|
|
# (push + pull_request, path-filtered), same "one job per verification layer" structure,
|
|
# and — like ios.yml — deliberately NO `concurrency:` group, so the two workflows behave
|
|
# the same way on rapid pushes.
|
|
#
|
|
# ── HONESTY NOTE (read before trusting a green badge) ────────────────────────────────────
|
|
# This repository's only remote is a self-hosted Gitea instance, so GitHub Actions has
|
|
# NEVER run here and this file has never been executed by the platform. What IS verified:
|
|
# every command below was run locally on macOS against the same Gradle build
|
|
# (`./gradlew test :app:assembleDebug koverVerify koverLog` + the androidTest compile),
|
|
# and the YAML parses. What is NOT verified: the runner-side pieces — action resolution,
|
|
# the preinstalled Android SDK layout on `ubuntu-latest`, and the emulator leg.
|
|
#
|
|
# ── Why no `src/**` path trigger (ios.yml HAS one) ───────────────────────────────────────
|
|
# ios.yml watches `src/**` because it owns `integration-tests`, a Swift layer that boots the
|
|
# real Node server and would catch client↔server protocol drift. Android has no equivalent
|
|
# leg (android/README.md, "DEFERRED — end-to-end access token": nothing here starts a server
|
|
# with WEBTERM_TOKEN set and completes a cookie-gated WS upgrade). Triggering on `src/**`
|
|
# would therefore burn runner minutes without checking a single contract. When an Android
|
|
# server-contract leg is added, add `src/**` here at the same time.
|
|
name: android
|
|
|
|
on:
|
|
push:
|
|
paths:
|
|
- "android/**"
|
|
- ".github/workflows/android.yml"
|
|
pull_request:
|
|
paths:
|
|
- "android/**"
|
|
- ".github/workflows/android.yml"
|
|
# Manual trigger for the instrumented (device) leg below, which is dispatch-only.
|
|
workflow_dispatch:
|
|
|
|
jobs:
|
|
# Layer 1 — everything that runs without a device. This is exactly the green gate
|
|
# android/README.md documents ("./gradlew test :app:assembleDebug koverVerify"), plus a
|
|
# compile-only pass over the instrumented sources so they cannot silently rot.
|
|
build-and-test:
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 45
|
|
defaults:
|
|
run:
|
|
working-directory: android
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
# jvmToolchain(17) is declared by every module; Gradle resolves it from the
|
|
# installed JDKs, so 17 must actually be present (the runner image's default may
|
|
# be a different major).
|
|
- uses: actions/setup-java@v4
|
|
with:
|
|
distribution: temurin
|
|
java-version: "17"
|
|
cache: gradle
|
|
|
|
# :app/:terminal-view/:host-registry/:client-tls-android compile against SDK 36 with
|
|
# build-tools 36.0.0 (android/README.md "Android SDK setup"). The runner image ships an
|
|
# Android SDK, but which platforms it holds drifts image to image — so install what the
|
|
# build actually needs instead of assuming, and FAIL LOUDLY if there is no sdkmanager.
|
|
- name: Install the Android SDK packages this build compiles against
|
|
run: |
|
|
# `set -eu` WITHOUT pipefail on purpose: `yes | sdkmanager --licenses` kills `yes`
|
|
# with SIGPIPE (141) as soon as the prompts stop, and pipefail would report that
|
|
# normal ending as a failed step.
|
|
set -eu
|
|
sdkmanager_bin="$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager"
|
|
if [ ! -x "$sdkmanager_bin" ]; then
|
|
sdkmanager_bin="$(ls -1 "$ANDROID_HOME"/cmdline-tools/*/bin/sdkmanager 2>/dev/null | tail -n 1 || true)"
|
|
fi
|
|
if [ ! -x "$sdkmanager_bin" ]; then
|
|
echo "::error title=No sdkmanager::none found under $ANDROID_HOME/cmdline-tools — the Android SDK is not usable on this runner" >&2
|
|
exit 1
|
|
fi
|
|
yes | "$sdkmanager_bin" --licenses > /dev/null
|
|
"$sdkmanager_bin" "platforms;android-36" "build-tools;36.0.0" "platform-tools"
|
|
|
|
# 10 modules of JVM unit tests (JUnit 5 + coroutines-test + Turbine + MockK). Includes
|
|
# the Android-library modules' testDebugUnitTest and :app's — no device needed.
|
|
- name: JVM unit tests (all modules)
|
|
run: ./gradlew test
|
|
|
|
# The APK build is its own step: a Kotlin/KSP/Hilt/Compose breakage that unit tests do
|
|
# not reach (resources, manifest merge, DI graph assembly) must not hide behind them.
|
|
- name: Debug APK builds
|
|
run: ./gradlew :app:assembleDebug
|
|
|
|
# Coverage floor: >=80% line coverage on the four modules that apply the Kover plugin
|
|
# (:wire-protocol, :session-core, :api-client, :client-tls). koverLog prints the actual
|
|
# percentages into the run log so a near-miss is visible before it becomes a failure.
|
|
# The Android-framework modules are NOT gated (no Kover on them) — a known gap.
|
|
- name: Coverage gate (Kover, >= 80% on the four gated modules)
|
|
run: ./gradlew koverVerify koverLog
|
|
|
|
# The instrumented sources cannot RUN here, but they must keep COMPILING: they are the
|
|
# only assertion that the access token is ciphertext at rest (TinkAccessTokenStoreTest)
|
|
# and that the AndroidKeyStore import path works, so a silent compile break would delete
|
|
# that coverage without anyone noticing.
|
|
- name: Instrumented test sources still compile
|
|
run: ./gradlew :host-registry:assembleDebugAndroidTest :client-tls-android:assembleDebugAndroidTest
|
|
|
|
- name: Upload test + coverage reports
|
|
if: failure()
|
|
uses: actions/upload-artifact@v4
|
|
with:
|
|
name: android-reports
|
|
path: |
|
|
android/*/build/reports/tests/**
|
|
android/*/build/test-results/**
|
|
android/*/build/reports/kover/**
|
|
retention-days: 7
|
|
if-no-files-found: ignore
|
|
|
|
# Layer 2 — instrumented (device) tests. DISPATCH-ONLY, on purpose.
|
|
#
|
|
# What is at stake: `TinkAccessTokenStoreTest` is the ONLY test anywhere that proves the
|
|
# frozen at-rest properties of the access token (ios-completion §1.1) — that a fresh store
|
|
# instance decrypts it after a cold start, and that the bytes on disk are ciphertext rather
|
|
# than the token in the clear. Tink's AEAD + the AndroidKeystore-wrapped master key have no
|
|
# JVM double, so this can only run on a device or emulator.
|
|
#
|
|
# Why it is not on push: this workflow has never executed on any runner (see the honesty
|
|
# note at the top), and an emulator leg is the single most fragile thing in Android CI —
|
|
# it needs KVM, a system image download, and an AVD boot. Wiring an unvalidated, ~15-minute,
|
|
# KVM-dependent job into every push would either block the repo on a leg nobody has seen
|
|
# pass, or push people to make it non-blocking — and a leg that cannot fail is a false green
|
|
# (the same trap ios.yml's `ios17-floor-tests` comment calls out).
|
|
#
|
|
# THEREFORE: run it by hand from the Actions tab (workflow_dispatch) whenever the token
|
|
# store, DataStore stores or the AndroidKeyStore importer change. Once it has been seen
|
|
# green on a real runner, promote it by deleting the `if:` below.
|
|
#
|
|
# MANUAL EQUIVALENT (the documented path today — android/README.md "DEFERRED — instrumented
|
|
# tests", DEVICE_QA_CHECKLIST.md): with a device/emulator attached,
|
|
# cd android && ANDROID_HOME=<sdk> ./gradlew :host-registry:connectedDebugAndroidTest \
|
|
# :client-tls-android:connectedDebugAndroidTest
|
|
# (StrongBox falls back to software keys on an emulator, so hardware-backed key claims still
|
|
# need real hardware.)
|
|
instrumented-tests:
|
|
if: github.event_name == 'workflow_dispatch'
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 60
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
- uses: actions/setup-java@v4
|
|
with:
|
|
distribution: temurin
|
|
java-version: "17"
|
|
cache: gradle
|
|
- name: Enable KVM for the emulator
|
|
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
|
|
# API 29 == the app's minSdk: the deployment floor is where at-rest crypto behaviour is
|
|
# most likely to differ (mirrors ios.yml's iOS 17 floor leg).
|
|
- name: connectedDebugAndroidTest on an API 29 emulator
|
|
uses: reactivecircus/android-emulator-runner@v2
|
|
with:
|
|
api-level: 29
|
|
target: google_apis
|
|
arch: x86_64
|
|
working-directory: android
|
|
script: ./gradlew :host-registry:connectedDebugAndroidTest :client-tls-android:connectedDebugAndroidTest
|
|
- name: Upload instrumented reports
|
|
if: always()
|
|
uses: actions/upload-artifact@v4
|
|
with:
|
|
name: android-instrumented-reports
|
|
path: android/*/build/reports/androidTests/**
|
|
retention-days: 7
|
|
if-no-files-found: ignore
|